Poplar是一個社交主題的內容社區,但自身並不作社區,旨在提供可快速二次開發的開源基礎套件。前端基於React Native與Redux構建,後端由Spring Boot、Dubbo、Zookeeper組成微服務對外提供一致的API訪問。html
https://github.com/lvwangbeta/Poplar前端
React Native雖然提供跨平臺解決方案,但並未在性能與開發效率上作出過分妥協,尤爲是對於有JS與CSS基礎的開發人員入手不會很難,不過JSX語法糖須要必定的適應時間,至於DOM結構與樣式和JS處理寫在一塊兒是否喜歡就見仁見智了,可這也是一個強迫你去模塊化解耦的比較好的方式。因爲React組件的數據流是單向的,所以會引入一個很麻煩的問題,組件之間很難高效通訊,尤爲是兩個層級很深的兄弟節點之間通訊變得異常複雜,對上游全部父節點形成傳遞污染,維護成本極高。爲此Poplar引入了Redux架構,統一管理應用狀態。java
APP由5個基礎頁面構成,分別是Feed信息流主頁(MainPage)、探索發現頁面(ExplorePage)、個人帳戶詳情頁(MinePage)、狀態建立於發送頁(NewFeed)、登陸註冊頁面(LoginRegPage)等。頁面又由基礎組件組成,如Feed列表、Feed詳情、評論、標籤、相冊等等。若是與服務器交互,則統一交由API層處理。mysql
頁面底部由TabNavigator
包含5個TabNavigator.Item
構成,分別對應基礎頁面,若是用戶未登陸,則在點擊主頁或新增Tab時呼出登陸註冊頁面。react
引入Redux並非趕潮流,並且早在2014年就已經提出了Flux的概念。使用Redux主要是不得不用了,Poplar組件結構並不是特別複雜,但嵌套關係較多,並且須要同時支持登陸與非登陸狀況的信息流訪問,這就須要一個統一的狀態管理器來協調組件之間的通訊和狀態更新,而Redux很好的解決了這個問題。git
這裏不枯燥的講解Redux的架構模型了,而是以Poplar中的登陸狀態爲例來簡單說下Redux在Poplar項目中是如何使用的。github
Poplar使用React-Redux庫,一個將Redux架構在React的實現。web
在未登陸狀況下,若是用戶點擊Feed流頁面會彈出登陸/註冊頁面,登陸或註冊成功以後頁面收回,同時刷新出信息流內容。下圖中的App組件是登陸頁面和信息流主頁兄弟節點的共同父組件。redis
這個需求看似簡單,但若是沒有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會第一時間通知你。
以實際的代碼爲例來說解下次場景的React-Redux實現:
在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
showNewFeedPage
props與Redux的action創建了映射關係。調用isLogin實際調用的是Redux中的store.dispatch(isLogin)
action,dispatch完成對action到reducer的分發。
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
了。
組件與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}/>}
poplar做爲一個總體Maven項目,頂層不具有業務功能也不包含代碼,對下層提供基礎的pom依賴導入
poplar-api有着兩重身份:API網關接收渠道層請求路由轉發、做爲微服務消費者組織提供者服務調用完成服務串聯
poplar-user-service: 微服務提供者,提供註冊、登陸、用戶管理等服務
poplar-feed-service: 微服務提供者,提供feed建立、生成信息流等服務
poplar-notice-service: 微服務提供者, 提供通知消息服務
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-common
和poplar
:
mvn clean install -pl poplar-api -am
執行構建發現Maven將poplar-api
依賴的poplar-common
和poplar
優先構建以後再構建本身:
[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
上面所述的服務提供者和消費者依託於Dubbo實現遠程調用,但還須要一個註冊中心,來完成服務提供者的註冊、通知服務消費者的任務,Zookeeper就是一種註冊中心的實現,poplar使用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
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就完成了對註冊中心的監控設置
微服務的提供者和消費者開發模式與以往的單體架構應用雖有不一樣,但邏輯關係大同小異,只是引入了註冊中心須要消費者和提供者配合實現一次請求,這就必然須要在二者之間協商接口和模型,保證調用的可用。
文檔以用戶註冊爲例展現從渠道調用到服務提供者、消費者和公共模塊發佈的完整開發流程。
poplar-common做爲公共模塊定義了消費者和提供者都依賴的接口和模型, 微服務發佈時才能夠被正常訪問到
定義用戶服務接口
public interface UserService { String register(String username, String email, String password); }
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
前面已經說過,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
若是以上步驟都已作完,一個完整的微服務架構基本已搭建完成,能夠開始coding業務代碼了,爲何還要再作Docker化改造?首先隨着業務的複雜度增高,可能會引入新的微服務模塊,在開發新模塊的同時提供一個穩定的外圍環境仍是頗有必要的,若是測試環境不理想,能夠本身啓動必要的docker容器,節省編譯時間;另外減小環境遷移帶來的程序運行穩定性問題,便於測試、部署,爲持續集成提供更便捷、高效的部署方式。
在poplar根目錄執行 build.sh
可實現poplar包含的全部微服務模塊的Docker化和一鍵啓動:
cd poplar && ./build.sh
若是你有耐心,可看下以下兩個小章節,是如何實現的
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的skipDockerBuild
爲true
,如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] ------------------------------------------------------------------------
因爲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
至此,poplar項目的後端已完整的構建和啓動,對外提供服務,客戶端(不管是Web仍是App)看到只有一個統一的API。