Poplar是一個社交主題的內容社區,但自身並不做社區,旨在提供可快速二次開發的開源基礎套件。前端基於React Native與Redux構建,后端由Spring Boot、Dubbo、Zookeeper組成微服務對外提供一致的API訪問。
https://github.com/lvwangbeta/Poplar
前端React Native & Redux
React Native雖然提供跨平台解決方案,但並未在性能與開發效率上做出過度妥協,尤其是對於有JS與CSS基礎的開發人員入手不會很難,不過JSX語法糖需要一定的適應時間,至於DOM結構與樣式和JS處理寫在一起是否喜歡就見仁見智了,可這也是一個強迫你去模塊化解耦的比較好的方式。由於React組件的數據流是單向的,因此會引入一個很麻煩的問題,組件之間很難高效通信,尤其是兩個層級很深的兄弟節點之間通信變得異常復雜,對上游所有父節點造成傳遞污染,維護成本極高。為此Poplar引入了Redux架構,統一管理應用狀態。
模塊化
APP由5個基礎頁面構成,分別是Feed信息流主頁(MainPage)、探索發現頁面(ExplorePage)、我的賬戶詳情頁(MinePage)、狀態創建於發送頁(NewFeed)、登錄注冊頁面(LoginRegPage)等。頁面又由基礎組件組成,如Feed列表、Feed詳情、評論、標簽、相冊等等。如果與服務器交互,則統一交由API層處理。
頁面底部由TabNavigator
包含5個TabNavigator.Item
構成,分別對應基礎頁面,如果用戶未登錄,則在點擊主頁或新增Tab時呼出登錄注冊頁面。
Redux
引入Redux並不是趕潮流,而且早在2014年就已經提出了Flux的概念。使用Redux主要是不得不用了,Poplar組件結構並非特別復雜,但嵌套關系較多,而且需要同時支持登錄與非登錄情況的信息流訪問,這就需要一個統一的狀態管理器來協調組件之間的通信和狀態更新,而Redux很好的解決了這個問題。
這里不枯燥的講解Redux的架構模型了,而是以Poplar中的登錄狀態為例來簡單說下Redux在Poplar項目中是如何使用的。
Poplar使用React-Redux庫,一個將Redux架構在React的實現。
1. 場景描述
在未登錄情況下,如果用戶點擊Feed流頁面會彈出登錄/注冊頁面,登錄或注冊成功之后頁面收回,同時刷新出信息流內容。下圖中的App組件是登錄頁面和信息流主頁兄弟節點的共同父組件。
這個需求看似簡單,但如果沒有Redux,在React中實現起來會很蹩腳而且會冗余很多無用代碼調用。
首先我們看下在沒有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
showNewFeedPage
props與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}/>}
后端微服務架構
項目構建 & 開發
1. 項目結構
poplar作為一個整體Maven項目,頂層不具備業務功能也不包含代碼,對下層提供基礎的pom依賴導入
poplar-api有着兩重身份:API網關接收渠道層請求路由轉發、作為微服務消費者組織提供者服務調用完成服務串聯
poplar-user-service: 微服務提供者,提供注冊、登錄、用戶管理等服務
poplar-feed-service: 微服務提供者,提供feed創建、生成信息流等服務
poplar-notice-service: 微服務提供者, 提供通知消息服務
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-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
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就完成了對注冊中心的監控設置
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的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] ------------------------------------------------------------------------
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
至此,poplar項目的后端已完整的構建和啟動,對外提供服務,客戶端(無論是Web還是App)看到只有一個統一的API。