从React Native到微服务,落地一个全栈解决方案

framework

Poplar是一个社交主题的内容社区,但自身并不作社区,旨在提供可快速二次开发的开源基础套件。前端基于React Native与Redux构建,后端由Spring Boot、Dubbo、Zookeeper组成微服务对外提供一致的API访问。html

https://github.com/lvwangbeta/Poplar前端

前端React Native & Redux

framework

React Native虽然提供跨平台解决方案,但并未在性能与开发效率上作出过分妥协,尤为是对于有JS与CSS基础的开发人员入手不会很难,不过JSX语法糖须要必定的适应时间,至于DOM结构与样式和JS处理写在一块儿是否喜欢就见仁见智了,可这也是一个强迫你去模块化解耦的比较好的方式。因为React组件的数据流是单向的,所以会引入一个很麻烦的问题,组件之间很难高效通讯,尤为是两个层级很深的兄弟节点之间通讯变得异常复杂,对上游全部父节点形成传递污染,维护成本极高。为此Poplar引入了Redux架构,统一管理应用状态。java

模块化

framework

APP由5个基础页面构成,分别是Feed信息流主页(MainPage)、探索发现页面(ExplorePage)、个人帐户详情页(MinePage)、状态建立于发送页(NewFeed)、登陆注册页面(LoginRegPage)等。页面又由基础组件组成,如Feed列表、Feed详情、评论、标签、相册等等。若是与服务器交互,则统一交由API层处理。mysql

framework

页面底部由TabNavigator包含5个TabNavigator.Item构成,分别对应基础页面,若是用户未登陆,则在点击主页或新增Tab时呼出登陆注册页面。react

Redux

引入Redux并非赶潮流,并且早在2014年就已经提出了Flux的概念。使用Redux主要是不得不用了,Poplar组件结构并不是特别复杂,但嵌套关系较多,并且须要同时支持登陆与非登陆状况的信息流访问,这就须要一个统一的状态管理器来协调组件之间的通讯和状态更新,而Redux很好的解决了这个问题。git

这里不枯燥的讲解Redux的架构模型了,而是以Poplar中的登陆状态为例来简单说下Redux在Poplar项目中是如何使用的。github

Poplar使用React-Redux库,一个将Redux架构在React的实现。web

1. 场景描述

在未登陆状况下,若是用户点击Feed流页面会弹出登陆/注册页面,登陆或注册成功以后页面收回,同时刷新出信息流内容。下图中的App组件是登陆页面和信息流主页兄弟节点的共同父组件。redis

framework

这个需求看似简单,但若是没有Redux,在React中实现起来会很蹩脚并且会冗余不少无用代码调用。spring

首先咱们看下在没有Redux的状况下是如何实现这一业务流程的?

在点击Tabbar的第一个Item也就是信息流页签时,要作用户是否登陆检查,这个检查能够经过查看应用是否本地化存储了token或其余验签方式验证,若是未登陆,须要主动更新App组件的state状态,同时将这个状态修改经过props的方式传递给LoginPage,LoginPage得知有新的props传入后更新本身的state:{visible:true}来呼出本身,若是客户输入登陆信息而且登陆成功,则须要将LoginPage的state设置为{visible:false}来隐藏本身,同时回调App传给它的回调函数来告诉父附件用户已经登陆成功,咱们算一下这仅仅是两个组件之间的通讯就要消耗1个props变量1个props回调函数和2个state更新,到这里只是完成了LoginPage通知App组件目前应用应该处于已登陆状态,可是尚未刷新出用户的Feed流,由于此时MainPage还不知道用户已登陆,须要App父组件来告知它已登陆请刷新,可怎样通知呢?React是数据流单向的,要想让下层组件更新只能传递变化的props属性,这样就又多了一个props属性的开销,MainPage更新关联的state同时刷新本身获取Feed流,这才最终完成了一次登陆后的MainPage信息展现。经过上面的分析能够看出Poplar在由未登陆到登陆的状态转变时冗余了不少可是又无法避免的参数传递,由于兄弟节点LoginPage与MainPage之间没法简单的完成通讯告知彼此的状态,就须要App父组件这个桥梁来先向上再向下的传递消息。

再来看下引入Redux以后是如何完成这一一样的过程的:

仍是在未登陆状况下点击主页,此时Poplar因为Redux的引入已经为应用初始了全局登陆状态{status: 'NOT_LOGGED_IN'},当用户登陆成功以后会将该状态更新为{status: 'LOGGED_IN'},同时LoginPage与此状态进行了绑定,Redux会第一时间通知其更新组件本身的状态为{visible:false}。与此同时App也绑定了这个由Redux管理的全局状态,所以也一样能够得到{status: 'LOGGED_IN'}的通知,这样就能够很简单的在客户登陆以后隐藏LoginPage显示MainPage,是否是很简单也很神奇,彻底不用依赖参数的层层传递,组件想要得到哪一个全局状态就与其关联就好,Redux会第一时间通知你。

2. 实现

以实际的代码为例来说解下次场景的React-Redux实现:

connect

在App组件中,经过connect方法将UI组件生成Redux容器组件,能够理解为架起了UI组件与Redux沟通的桥梁,将store于组件关联在一块儿。

import {showLoginPage, isLogin} from './actions/loginAction'; import {showNewFeedPage} from './actions/NewFeedAction'; export default connect((state) => ({ status: state.isLogin.status, //登陆状态 loginPageVisible: state.showLoginPage.loginPageVisible }), (dispatch) => ({ isLogin: () => dispatch(isLogin()), showLoginPage: () => dispatch(showLoginPage()), showNewFeedPage: () => dispatch(showNewFeedPage()), }))(App)

connect方法的第一个参数是mapStateToProps函数,创建一个store中的数据到UI组件props对象的映射关系,只要store更新了就会调用mapStateToProps方法,mapStateToProps返回一个对象,是一个UI组件props与store数据的映射。上面代码中,mapStateToProps接收state做为参数,返回一个UI组件登录状态与store中state的登录状态的映射关系以及一个登录页面是否显示的映射关系。这样App组件状态就与Redux的store关联上了。

第二个参数mapDispatchToProps函数容许将action做为props绑定到组件上,返回一个UI组件props与Redux action的映射关系,上面代码中App组件的isLogin showLoginPage showNewFeedPageprops与Redux的action创建了映射关系。调用isLogin实际调用的是Redux中的store.dispatch(isLogin) action,dispatch完成对action到reducer的分发。

Provider

connect中的state是如何传递进去的呢?React-Redux 提供Provider组件,可让容器组件拿到state

import React, { Component } from 'react'; import { Provider } from 'react-redux'; import configureStore from './src/store/index'; const store = configureStore(); export default class Root extends Component { render() { return ( <Provider store={store}> <Main /> </Provider> ) } }

上面代码中,Provider在根组件外面包了一层,这样一来,App的全部子组件就默认均可以拿到state了。

Action & Reducer

组件与Redux全局状态的关联已经搞定了,可如何实现状态的流转呢?登陆状态是如何扩散到整个应用的呢?

这里就须要Redux中的Action和Reducer了,Action负责接收UI组件的事件,Reducer负责响应Action,返回新的store,触发与store关联的UI组件更新。

export default connect((state) => ({ loginPageVisible: state.showLoginPage.loginPageVisible, }), (dispatch) => ({ isLogin: () => dispatch(isLogin()), showLoginPage: (flag) => dispatch(showLoginPage(flag)), showRegPage: (flag) => dispatch(showRegPage(flag)), }))(LoginPage) this.props.showLoginPage(false); this.props.isLogin();

在这个登陆场景中,如上代码,LoginPage将本身的props与store和action绑定,若是登陆成功,调用showLoginPage(false)action来隐藏自身,Reducer收到这个dispatch过来的action更新store状态:

//Action export function showLoginPage(flag=true) { if(flag == true) { return { type: 'LOGIN_PAGE_VISIBLE' } } else { return { type: 'LOGIN_PAGE_INVISIBLE' } } } //Reducer export function showLoginPage(state=pageState, action) { switch (action.type) { case 'LOGIN_PAGE_VISIBLE': return { ...state, loginPageVisible: true, } break; case 'LOGIN_PAGE_INVISIBLE': return { ...state, loginPageVisible: false, } break; default: return state; } }

同时调用isLogin这个action更新应用的全局状态为已登陆:

//Action export function isLogin() { return dispatch => { Secret.isLogin((result, token) => { if(result) { dispatch({ type: 'LOGGED_IN', }); } else { dispatch({ type: 'NOT_LOGGED_IN', }); } }); } } //Reducer export function isLogin(state=loginStatus, action) { switch (action.type) { case 'LOGGED_IN': return { ...state, status: 'LOGGED_IN', } break; case 'NOT_LOGGED_IN': return { ...state, status: 'NOT_LOGGED_IN', } break; default: return state; } }

App组件因为已经关联了这个全局的登陆状态,在reducer更新了此状态以后,App也会收到该更新,进而从新渲染本身,此时MainPage就会渲染出来了:

const {status} = this.props; return ( <TabNavigator> <TabNavigator.Item selected={this.state.selectedTab === 'mainTab'} renderIcon={() => <Image style={styles.icon} source={require('./imgs/icons/home.png')} />} renderSelectedIcon={() => <Image style={styles.icon} source={require('./imgs/icons/home_selected.png')} />} onPress={() => { this.setState({ selectedTab: 'mainTab' }); if(status == 'NOT_LOGGED_IN') { showLoginPage(); } } } > //全局状态已由NOT_LOGGED_IN变为LOGGED_IN {status == 'NOT_LOGGED_IN'?<LoginPage {...this.props}/>:<MainPage {...this.props}/>}

后端微服务架构

framework

项目构建 & 开发

1. 项目结构

project

poplar做为一个总体Maven项目,顶层不具有业务功能也不包含代码,对下层提供基础的pom依赖导入
poplar-api有着两重身份:API网关接收渠道层请求路由转发、做为微服务消费者组织提供者服务调用完成服务串联
poplar-user-service: 微服务提供者,提供注册、登陆、用户管理等服务
poplar-feed-service: 微服务提供者,提供feed建立、生成信息流等服务
poplar-notice-service: 微服务提供者, 提供通知消息服务

每一个子项目以Module方式单首创建
module

2. Maven聚合项目

Poplar由多个服务提供者、消费者和公共组件构成,他们之间的依赖关系既有关联关系又有父子从属关系, 为了简化配置也便于统一构建,须要创建合理的依赖。服务的提供者主要是Spring Boot项目,兼有数据库访问等依赖;服务的消费者一样是是Spring Boot项目,但因为是API层,须要对外提供接口,因此须要支持Controller; 服务消费者、提供者经过Dubbo完成调用,这也须要共用的Dubbo组件,因此咱们能够发现消费者、提供者共同依赖Spring Boot以及Dubbo,抽离出一个parent的pom便可,定义公共的父组件:

<groupId>com.lvwangbeta</groupId> <artifactId>poplar</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>pom</packaging> <name>poplar</name> <description>Poplar</description> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> ... </dependencies>

Poplar父组件除了引入公共的构建包以外,还须要声明其包含的子组件,这样作的缘由是在Poplar顶层构建的时候Maven能够在反应堆计算出各模块之间的依赖关系和构建顺序。咱们引入服务提供者和消费者:

 <modules>
    <module>poplar-common</module> <module>poplar-api</module> <module>poplar-feed-service</module> <module>poplar-user-service</module> </modules>

子组件的pom结构就变的简单许多了,指定parent便可,pom源为父组件的相对路径

<groupId>com.lvwangbeta</groupId> <artifactId>poplar-api</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <name>poplar-api</name> <description>poplar api</description> <parent> <groupId>com.lvwangbeta</groupId> <artifactId>poplar</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> <!-- lookup parent from repository --> </parent> 

还有一个公共构建包咱们并无说,它主要包含了消费者、提供者共用的接口、model、Utils方法等,不须要依赖Spring也没有数据库访问的需求,这是一个被其余项目引用的公共组件,咱们把它声明为一个package方式为jar的本地包便可,不须要依赖parent:

<groupId>com.lvwangbeta</groupId> <artifactId>poplar-common</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging>

在项目总体打包的时候,Maven会计算出其余子项目依赖了这个本地jar包就会优先将其打入本地Maven库。 在Poplar项目根目录执行mvn clean install查看构建顺序,能够看到各子项目并非按照咱们在Poplar-pom中定义的那样顺序执行的,而是Maven反应堆计算各模块的前后依赖来执行构建,先构建公共依赖common包而后构建poplar,最后构建各消费者、提供者。

[INFO] Reactor Summary:
[INFO]
[INFO] poplar-common ...................................... SUCCESS [ 3.341 s]
[INFO] poplar ............................................. SUCCESS [ 3.034 s]
[INFO] poplar-api ......................................... SUCCESS [ 25.028 s]
[INFO] poplar-feed-service ................................ SUCCESS [ 6.451 s]
[INFO] poplar-user-service ................................ SUCCESS [ 8.056 s]
[INFO] ------------------------------------------------------------------

若是咱们只修改了某几个子项目,并不须要全量构建,只须要用Maven的-pl选项指定项目同时-am构建其依赖的模块便可,咱们尝试单独构建poplar-api这个项目,其依赖于poplar-commonpoplar:

mvn clean install -pl poplar-api -am  

执行构建发现Maven将poplar-api依赖的poplar-commonpoplar优先构建以后再构建本身:

[INFO] Reactor Summary:
[INFO] [INFO] poplar-common ...................................... SUCCESS [ 2.536 s]
[INFO] poplar ............................................. SUCCESS [ 1.756 s]
[INFO] poplar-api ......................................... SUCCESS [ 28.101 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

3. Dubbo & Zookeeper

上面所述的服务提供者和消费者依托于Dubbo实现远程调用,但还须要一个注册中心,来完成服务提供者的注册、通知服务消费者的任务,Zookeeper就是一种注册中心的实现,poplar使用Zookeeper做为注册中心。

3.1 Zookeeper安装

下载解压Zookeeper文件

$ cd zookeeper-3.4.6  
$ mkdir data  

建立配置文件

$ vim conf/zoo.cfg

tickTime = 2000
dataDir = /path/to/zookeeper/data
clientPort = 2181
initLimit = 5
syncLimit = 2

启动

$ bin/zkServer.sh start

中止

$ bin/zkServer.sh stop 

3.2 Dubbo admin

Dubbo管理控制台安装

git clone https://github.com/apache/incubator-dubbo-ops
cd incubator-dubbo-ops && mvn package 

而后就能够在target目录下看到打包好的war包了,将其解压到tomcat webapps/ROOT目录下(ROOT目录内容要提早清空),能够查看下解压后的dubbo.properties文件,指定了注册中心Zookeeper的IP和端口

dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.admin.root.password=root dubbo.admin.guest.password=guest

启动tomcat

./bin/startup.sh 	

访问


百度V认证 www.iis7.com/b/plc/?1-29.html

http://127.0.0.1:8080/   
 

这样Dubbo就完成了对注册中心的监控设置

dubbo-admin

4. 开发

微服务的提供者和消费者开发模式与以往的单体架构应用虽有不一样,但逻辑关系大同小异,只是引入了注册中心须要消费者和提供者配合实现一次请求,这就必然须要在二者之间协商接口和模型,保证调用的可用。

文档以用户注册为例展现从渠道调用到服务提供者、消费者和公共模块发布的完整开发流程。

4.1 公共

poplar-common做为公共模块定义了消费者和提供者都依赖的接口和模型, 微服务发布时才能够被正常访问到
定义用户服务接口

public interface UserService { String register(String username, String email, String password); }

4.2 服务提供者

UserServiceImpl实现了poplar-common中定义的UserService接口

@Service
public class UserServiceImpl implements UserService { @Autowired @Qualifier("userDao") private UserDAO userDao; public String register(String username, String email, String password){ if(email == null || email.length() <= 0) return Property.ERROR_EMAIL_EMPTY; if(!ValidateEmail(email)) return Property.ERROR_EMAIL_FORMAT; ... }

能够看到这就是单纯的Spring Boot Service写法,可是@Service注解必定要引入Dubbo包下的,才可让Dubbo扫描到该Service完成向Zookeeper注册:

dubbo.scan.basePackages = com.lvwangbeta.poplar.user.service

dubbo.application.id=poplar-user-service dubbo.application.name=poplar-user-service dubbo.registry.address=zookeeper://127.0.0.1:2181 dubbo.protocol.id=dubbo dubbo.protocol.name=dubbo dubbo.protocol.port=9001

4.3 服务消费者

前面已经说过,poplar-api做为API网关的同时仍是服务消费者,组织提供者调用关系,完成请求链路。

API层使用@Reference注解来向注册中心请求服务,经过定义在poplar-common模块中的UserService接口实现与服务提供者RPC通讯

@RestController
@RequestMapping("/user") public class UserController { @Reference private UserService userService; @ResponseBody @RequestMapping("/register") public Message register(String username, String email, String password) { Message message = new Message(); String errno = userService.register(username, email, password); message.setErrno(errno); return message; } } 

application.properties配置

dubbo.scan.basePackages = com.lvwangbeta.poplar.api.controller

dubbo.application.id=poplar-api dubbo.application.name=poplar-api dubbo.registry.address=zookeeper://127.0.0.1:2181 

5.服务Docker化

若是以上步骤都已作完,一个完整的微服务架构基本已搭建完成,能够开始coding业务代码了,为何还要再作Docker化改造?首先随着业务的复杂度增高,可能会引入新的微服务模块,在开发新模块的同时提供一个稳定的外围环境仍是颇有必要的,若是测试环境不理想,能够本身启动必要的docker容器,节省编译时间;另外减小环境迁移带来的程序运行稳定性问题,便于测试、部署,为持续集成提供更便捷、高效的部署方式。

在poplar根目录执行 build.sh 可实现poplar包含的全部微服务模块的Docker化和一键启动:

cd poplar && ./build.sh

若是你有耐心,可看下以下两个小章节,是如何实现的

5.1 构建镜像

Poplar采用了将各微服务与数据库、注册中心单独Docker化的部署模式,其中poplar-dubbo-admin是dubbo管理控制台,poplar-api poplar-tag-service poplar-action-service poplar-feed-service poplar-user-service是具体的服务化业务层模块,poplar-redis poplar-mysql提供缓存与持久化数据支持,poplar-zookeeper为Zookeeper注册中心

poplar-dubbo-admin
poplar-api
poplar-tag-service
poplar-action-service
poplar-feed-service
poplar-user-service
poplar-redis
poplar-mysql
poplar-zookeeper

poplar-api poplar-tag-service poplar-action-service poplar-feed-service poplar-user-service业务层模块能够在pom.xml中配置docker-maven-plugin插件构建,在configuration中指定工做目录、基础镜像等信息可省去Dockerfile:

<plugin>
    <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>1.0.0</version> <configuration> <imageName>lvwangbeta/poplar</imageName> <baseImage>java</baseImage> <maintainer>lvwangbeta lvwangbeta@163.com</maintainer> <workdir>/poplardir</workdir> <cmd>["java", "-version"]</cmd> <entryPoint>["java", "-jar", "${project.build.finalName}.jar"]</entryPoint> <skipDockerBuild>false</skipDockerBuild> <resources> <resource> <targetPath>/poplardir</targetPath> <directory>${project.build.directory}</directory> <include>${project.build.finalName}.jar</include> </resource> </resources> </configuration> </plugin>

若是想让某个子项目不执行docker构建,可设置子项目pom.xml的skipDockerBuildtrue,如poplar-common为公共依赖包,不须要单独打包成独立镜像:

<skipDockerBuild>true</skipDockerBuild>

在poplar项目根目录执行以下命令,完成整个项目的业务层构建:

mvn package -Pdocker  -Dmaven.test.skip=true docker:build
[INFO] Building image lvwangbeta/poplar-user-service
Step 1/6 : FROM java
 ---> d23bdf5b1b1b
Step 2/6 : MAINTAINER lvwangbeta lvwangbeta@163.com
 ---> Running in b7af524b49fb
 ---> 58796b8e728d
Removing intermediate container b7af524b49fb
Step 3/6 : WORKDIR /poplardir
 ---> e7b04b310ab4
Removing intermediate container 2206d7c78f6b
Step 4/6 : ADD /poplardir/poplar-user-service-2.0.0.jar /poplardir/
 ---> 254f7eca9e94
Step 5/6 : ENTRYPOINT java -jar poplar-user-service-2.0.0.jar
 ---> Running in f933f1f8f3b6
 ---> ce512833c792
Removing intermediate container f933f1f8f3b6
Step 6/6 : CMD java -version
 ---> Running in 31f52e7e31dd
 ---> f6587d37eb4d
Removing intermediate container 31f52e7e31dd
ProgressMessage{id=null, status=null, stream=null, error=null, progress=null, progressDetail=null}
Successfully built f6587d37eb4d
Successfully tagged lvwangbeta/poplar-user-service:latest
[INFO] Built lvwangbeta/poplar-user-service
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

5.2 启动运行容器

因为poplar包含的容器过多,在此为其建立自定义网络poplar-netwotk

docker network create --subnet=172.18.0.0/16 poplar-network

运行以上构建的镜像的容器,同时为其分配同网段IP

启动Zookeeper注册中心

docker run --name poplar-zookeeper --restart always -d  --net poplar-network --ip 172.18.0.6  zookeeper 

启动MySQL

docker run --net poplar-network --ip 172.18.0.8  --name poplar-mysql -p 3307:3306 -e MYSQL_ROOT_PASSWORD=123456 -d  lvwangbeta/poplar-mysql

启动Redis

docker run --net poplar-network --ip 172.18.0.9 --name poplar-redis -p 6380:6379 -d redis

启动业务服务

docker run --net poplar-network --ip 172.18.0.2 --name=poplar-user-service -p 8082:8082 -t lvwangbeta/poplar-user-service

docker run --net poplar-network --ip 172.18.0.3 --name=poplar-feed-service -p 8083:8083 -t lvwangbeta/poplar-feed-service

docker run --net poplar-network --ip 172.18.0.4 --name=poplar-action-service -p 8084:8084 -t lvwangbeta/poplar-action-service

docker run --net poplar-network --ip 172.18.0.10 --name=poplar-api -p 8080:8080 -t lvwangbeta/poplar-api

dockerps

至此,poplar项目的后端已完整的构建和启动,对外提供服务,客户端(不管是Web仍是App)看到只有一个统一的API。

相关文章
相关标签/搜索