React最小系统的搭建
与Angular、Vue.js和微信小程序等开发一样,React也是一门数据驱动的语言(相对而言的dom驱动代表是jquery),其中Angular、Vue和React又称是新兴框架的三巨头。总的来说,React和Angular、Vue等的模式类似,一要学会其中一种,就可以快速入手其它任意一门。
1、搭建开发环境
开发环境基于nodejs,没有安装npm的需要先去了解前面的教程。
我们先全局安装create-react-app(react官网推荐的一个脚手架)
image安装完create-react-app后就可以搭建项目了,我们先找一个合适的位置,然后进入cmd中
image image比如我们要创建一个叫demo的项目,输入create-react-app demo,回车后等待一段时间,项目会自动创建好
image image等待下载(大概132M),需要一定的时间,看网速了
image image下载完成后cmd中进入到项目目录
image启动项目
image image image默认情况下,运行成功后会自动在浏览器中打开(http://localhost:3000/),样式是默认的
image如果发现端口被占用,可以先把其他3000端口停用,也可以把本项目的端口修改。修改本项目端口需要进入到(node_modules\react-scripts\scripts\start.js),然后搜索3000,修改掉就行(一般在第51行)
2、分析项目
接着我们来分析这个项目是如何构成的
imageimage》node_modules是npm模块,刚刚下载的132M模块大部分都在里面,其次我们人为安装的也会知道下载到里面(后面介绍安装路由模块和状态管理器)
》Public是公共资源,里面主要有首页、图标等(其中首页在开发模式下回自动导入相关的js)
》Src是和源码相关的,我们一般把开发的代码全部放在这里(将来会打包到build文件夹下)
》gitignore,看文件名就知道了,git的相关忽略配置,git会按照里面的配置自动忽略监听某些文件(默认就行,不用改,需要注意的是,该文件要在git初始化前创建,创建好修改不起作用,需要清除git缓存)
》Package-lock.json自动生成的文件
》Package.json项目配置文件(重要),我们项目的信息在这里有配置,例如版本和依赖等
》Readme.md, readme
》Build执行打包后生成的静态文件,可用于部署发布
接下来我们先看public/index.html
image发现是一个极其简单的静态页面,没有导入任何的css和js,不过有点奇怪的是部分路径使用了%PUBLIC_URL%,查看注释发现编译的时候回自动把它替换成public文件夹的路径。同时,在开发环境下,会生成一个缓存文件,缓存文件是index.html的副本,但是会自动导入相关的js和css
再接着我们来看src/index.js,它是项目的项目的入口文件,开发是从这个文件正式开始的。
imageIndex.js入口文件中导入了react的核心文件,同时也导入了样式和一个叫App的组件,还有一个registerServiceWorker文件。为了排除没必要的影响,我们可以不导入registerServiceWorker,并把最后一行删除,其实这个文件是在开发环境下利用缓存加快加载速度的一个服务,删掉没有影响。
这样下来,整个index.js的核心代码就是
image意思是以index.html下id为root的节点为作用域,实例化react,接着把root的内容全部替换成App组件
既然说到index.js入口文件里面已经说到App组件,那就不得不说App组件了。App组件在src文件夹下,代码如下
image通过代码可以看到App.js导入了react的核心代码和组件模块,然后定义了一个class继承Component组件(也就是定义了一个组件),最后把组件导出去供调用方使用。当然导出还可以改成下面的模式,是一样的:
image每一个组件都有很多钩子(类似Vue下vue组件的钩子),有组件初始化钩子、组件开始构建时的钩子、组件构建完成时的钩子等等,其次最最核心渲染钩子,也就是上面看到的render函数。Render函数最终需要返回一个jsx对象,里面包含html和相关绑定的变量,写法和angular的模板、vue组件以及handlebars等极其相似,这里就没必要继续展开了(有些细节需要注意的是在es6下class是关键字,所以html里面的class要改成className)。
导出组件后,其他调用方就可以使用,比如index.js下是这样使用的:
image如果代码开发完成了,脱离开发环境,代码是无法运行的,所以我们需要把代码打包,在cmd中输入npm run build即可,打包完成会项目目录下生成静态文件,上传到服务器部署即可
image3.项目组件划分
当然这样是远远不够的,离一个可用的系统还差一段距离,接下来我们添加一个简单的功能,同时当页面开始多的时候我们也需要用上路由。先来说说要开发的项目是怎么样的
我们要开发的系统有首页、文章列表和文章详情,而且页面的顶部和底部要固定,每个页面要有一致的效果显示,所以我们可以分成下列的组件:
》Header组件(顶部)
》Home组件(首页)
》Acticlelist组件(文章列表)
》Acticledetail组件(文章详情)
》Footer组件(底部)
然后分别创建acticle、common和default文件夹,active文件夹放和文章相关的组件,common放通用的资源(如公共方法、路由配置和状态管理等),default放默认的组件(如home、header和footer)。
image image image image紧接着在根组件App中导入刚刚创建的全部组件,然后把里面的jsx代码替换成于它们相关的,代码如下:
image image3、加载react路由
组件初步开发好了,但不能切换,我们要做的是一个单页应用,为了加快开发进度,决定采用路由(react-router-dom)。
首先还是去下载模块(同时保持到项目配置文件中)
image下载完成后发现package.json发生了变化
image接着我们去到根组件App里面导入路由模块,然后再进行相关配置(配置可问度娘)
image相关页面
image image4、React组件钩子(钩子函数、也可以叫生命周期)
和angular、vue、小程序等一样,react组件有众多的钩子,配合钩子可以开发出复杂的应用。下面列举一些常用的钩子,不了解什么是钩子函数的可以看我之前的文章。
钩子 ****执行时间 ****作用 ****Vue下对应的钩子
constructor() 组件初始化时(又称构造函数) 初始化组件数据 Data()
componentWillMount() 即将渲染前组件 加载前的预处理 beforeCreate()、created()
Render() 正在渲染时 返回jsx对象(html) 类似template
componentDidMount() 渲染完成后 渲染后执行 Mounted()
自定义钩子 手动调用 处理具体业务逻辑 Methods.自定义方法
下面以根组件为例,测试各个钩子
image image image接着分析一下代码
image在构造函数中,因为组件App要继承React的组件模块,所以在构造函数中要调用超类(父类)的构造函数,也就是圈出来的 super() (如果涉及组件间通讯,构造函数还可以传入props参数,后面介绍)。在一个组件中,数据存在状态state中,每当状态state发送改变,UI也会发生相应的变化(双向数据绑定)。但是和angular和vue不一样,react直接改变 this.state 不会触发UI重新渲染,必须调用 this.setState(obj) 才会重新渲染数据,这和微信小程序下的setData一模一样,开发时稍微调整下思路即可。
5、Redux状态管理
在任何一个大型用于下,状态管理都是必不可少,状态管理器可以规范我们的数据,但是在开始接受状态管理前先介绍一下react下的组件间通讯
在上一个演示的基础上,我们修改根组件App如下
image简单改动html部分(App.js)
接着我们需要在Header组件中接收刚刚的参数并把它渲染出来
image修改继承并获取参数(Header.js)
在Header组件的构造函数中,必须传递进props参数,否则无法接收到父组件传递进来的参数,获取参数时使用 this.props.log 即可获得到,此时页面如下
image image题外话:如果存在需要子组件需要调用父组件方法的需求,写法也是差不多,父组件传递时传递方法名即可,但是有一点需要注意的,默认情况下,被调用的方法(假设是App下的debug方法)的上下文是props,取不到父组件的数据,如果需要去到父组件的数据,需要在父组件的构造函数中绑定上下文,如
image现在可以正式介绍状态管理了,但是需要注意状态管理不是必需的,不推荐滥用,只有在需要全局共享数据等情况下才建议使用,在不合适场景下使用反而会导致代码难以维护(比如可以用组件间通讯解决问题)。
我们使用redux状态管理器(在react下叫react-redux),我们先安装redux和react-redux
image然后我们在common文件夹下创建一个reducers.js文件,它的作用是定义redux的全局状态,代码如下
image代码中我们导入了redux模块,并且自定义了一个全局状态onlione(用来设置在线状态)模板,将来调用SET_ONLINE即可设置它的值(默认为false),调用获取快照可获取到其当前状态。
然后我们去根组件App中定义仓库存放状态,当然该导入还是得先导入,代码如下:
image状态仓库定义好了,我们要去使用它,使用前得先设置仓库的使用范围,我们用Provider标签设置其范围,然后绑定上仓库store,代码如下:
image仓库是绑定上去了,但是怎么获取仓库内的状态呢,我们叫获取快照,也就是获取当前的状态值,方法为 store.getState() ,我们测试一下
image image问题又来了,怎么设置状态值呢,这就和我们以前开发的不太一样了,我们需要调用dispatch方法,如下
imagestore.dispatch({type:"SET_ONLINE", payload: true});
状态的确改变了,使用状态里面的数据时直接绑定到state上即可,如
image image但是问题又来了,发现状态是更新了,但是UI没有发生变化,我们现在需要监听仓库里面状态的变化,然后实时setState,实现如下:
image问题又来了,在子组件中如何获取到 online 或修改它呢(情况是希望在header组件中完成登录,然后其他组件能够获取到登录状态和用户信息)?获取和修改就需要用到我们前面说的组件间通讯了,我们把online参数传递进Header组件中,然后Header组件通过this.props.dispatch更新状态,此时App.js:
image主要修改UI和屏蔽了更新状态的代码
然后Header组件改动如下:
image image很不幸,运行时发现报错, this.props 中并不存在 dispatch 属性
为什么会报错呢,因为我们还没把组件和redux仓库连接起来,我们要对Header组件再做一个小改动,连接一下仓库即可:
image image完美没毛病
那么问题又来了,刚刚的操作其实是父子组件间的通讯,是在父组件(根组件)中监听store的改动,然后动态绑定到子组件的props上,在非父子组件中又如何获取到store里面的值呢,这个时候我们就需要订阅store了,也就是在组件连接上redux的时候,给它绑定上订阅事件,当store发送改变时,组件重新渲染。比如说我们要在路由Home中获取到online这个状态,我们需要先在Home组件中连接Redux,然后订阅store中online的更新,下面是具体实现
image然后Home组件中通过this.props.online即可拿到store中的值,同样,每当store的online发送变化时,Home组件会重新渲染online(这个过程就叫订阅),更多方法可以查看链接:http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.html
PS:this.props一般是外部传递过来的,不可修改,而this.state是本组件的状态,可修改,要注意两者区别
现在总结一下react下redux的使用:
1.下载redux和react-redux
2.创建一个状态模板
3.根组件定义状态仓库
4.根组件添加仓库的作用域并绑定上去()
5.根组件通过store.subscribe监听状态变化,然后用store.getState获取状态快照,最后绑定到自己的state上
6.子组件需要更新全局状态,需要把组件和redux连接起来(connect,不需要更新可不连接),然后通过 this.props.dispatch 更新全局状态,需要获取状态从父组件获取即可
7.如果组价需要监听(订阅)store的更新,根组件使用store.subscribe,而其他组件可以使用订阅的方式来实现。
6、React交互事件
一个健壮的系统怎么可以没有交互呢,react下从很多交互事件,如click、change等,我们先在Header组件中添加一个组件,然后实现表单数据绑定,这时候开发就和angular和Vue不一样了,而于微信小程序更像,因为在表单中无法实现双向数据绑定,需要自己去监听表单change,代码如下:
image开发过小程序就会发现这两者有异曲同工之妙(header.js)
image接着我们给它添加一个搜索按钮,并绑定点击事件(当然也需要绑定点击事件的上下文)
image要注意的是jsx里面的html不是真实的dom,如果需要获取dom元素,需要给标签添加refs属性,然后通过this.refs[...]即可获取到元素的dom元素
下面我们可以把上面的代码稍微修改一下
image image7、渲染服务端数据
除了事件交互,更重要的还有和服务器的交互,可以react并没有提供官方的ajax交互,我们可以使用jquery的ajax,也可以采用其他库,当然还可以选择h5下的fetch,下面是fetch的一个简要代码
image一般来说,不用fetch,而是采用axios插件,下面简单介绍一下axios的使用和封装http拦截器(vue、angular和小程序中也封装http拦截器,会方便很多)
import axios from 'axios';
import JavascriptCommon from './javascript.common';
import { Toast } from 'antd-mobile';
//基础设置
axios.defaults.timeout = 1000 * 60 * 2;
axios.defaults.baseURL = "你的http前缀";
axios.defaults.transformRequest = [
function (data) { let ret = "" for (let it in data) { ret += encodeURIComponent(it) + "=" + encodeURIComponent(data[it]) + "&" } return ret }
];
//微信授权
axios.authorization = (appid, url, state) => {
let _url = window.location.hash.toLocaleLowerCase(); let flag = _url.indexOf("#/authorization/") === -1 && _url.indexOf("#/recruit") === -1 && _url.indexOf("#/valuation") === -1; if (flag) { window.location.href = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + appid + "&redirect_uri=" + url + "&response_type=code&scope=snsapi_userinfo&state=" + state + "#wechat_redirect"; }
};
// http请求拦截器
axios.interceptors.request.use(function (config) {
config.headers = config.headers || {}; //模拟登录需要设置sessionId,具体值参考登录后返回sessionid //JavascriptCommon.SetUserSessionId("**********"); let sessionId = JavascriptCommon.GetUserSessionId(); if (sessionId) { if (!config.headers["SESSIONID"]) { config.headers["SESSIONID"] = sessionId; } } JavascriptCommon.AjaxLoading(true); return config;
}, function (err) {
return Promise.reject(err);
});
// http响应拦截器
axios.interceptors.response.use(function (res) {
JavascriptCommon.AjaxLoading(false); try { if (typeof res.data === "object") { if (res.data.status) { if (res.data.status === -200) { axios.authorization("appid", "编码后的回调地址", "weixin_h5"); return Promise.reject(res); } else if (res.data.status === -201) { Toast.info(res.data.msg, 1.2); window.location.hash = "/supplement"; return Promise.reject(res); } } } } catch (err) { console.log("请求异常", err); } return res;
}, function (err) {
JavascriptCommon.AjaxLoading(false); return Promise.reject(err);
});
export default axios;//最后导出模块
使用方式如下:
import axios from './common/axiosConfig'; //导入封装的模块
//发起请求
axios.get("/interest/category").then((res) => {
store.dispatch({ type: "SET_CATEGORY", payload: res.data });
});
网友评论