Poplar is a social themed content community, but not a community itself, designed to provide an open source infrastructure suite that can be quickly redeveloped. The front-end is built based on React Native and Redux, and the back-end consists of Spring Boot, Dubbo, and Zookeeper to provide consistent API access.
Github.com/lvwangbeta/…
React Native & Redux front end
React Native offers a cross-platform solution, but it doesn’t compromise too much on performance and development efficiency, especially for developers with basic JS and CSS skills. However, JSX syntax takes some time to get used to. As for DOM structure and style and JS processing, it’s a matter of opinion. But it’s also a good way to force you to decouple modules. Because the React component’s data flow is one-way, it introduces a very troublesome problem. It is difficult for components to communicate efficiently. Especially, the communication between two sibling nodes at a deep level becomes extremely complicated, causing transmission pollution to all upstream parent nodes and high maintenance costs. For this, Poplar introduces the Redux architecture to centrally manage application states.
modular
The APP is made up of five basic pages, including the Feed flow homepage (MainPage), the ExplorePage (ExplorePage), my account details page (MinePage), the status created from send page (NewFeed), and the LoginRegPage (LoginRegPage). The page in turn consists of basic components, such as Feed lists, Feed details, comments, tags, photo albums, and so on. If you interact with the server, it’s all handled by the API layer.
At the bottom of the page, TabNavigator consists of five TabNavigator. items, respectively corresponding to the basic page. If the user does not log in, the login registration page will be called when clicking the main page or adding a Tab.
Redux
Introducing Redux isn’t jumping on the bandwagon, and the concept of Flux was first proposed back in 2014. The Poplar component structure is not particularly complex, but it has a lot of nested relationships and needs to support both logon and non-logon traffic. This requires a unified state manager to coordinate communication and status updates between components, and Redux provides a good solution to this problem.
Instead of going into the architectural model for Redux, here’s a quick look at how Redux is used in Poplar, using the login status in Poplar as an example.
Poplar uses the React-Redux library, an implementation of the Redux architecture in React.
1. Scenario description
If the user does not log in, the login/registration page will pop up if the user clicks the Feed flow page. After the login or registration is successful, the page will be recalled and the information flow content will be refreshed. The App component in the figure below is the common parent of the sibling nodes of the login page and the home page of the information flow.
This requirement may seem simple, but without Redux, it would be clunky to implement in React and would make a lot of unnecessary code calls redundant.
Let’s first look at how this business process is implemented without Redux.
When clicking the first Item of Tabbar, namely the information flow TAB, check whether the user is logged in or not. This check can be verified by checking whether the application stores tokens locally or by other means of verification. If the user is not logged in, it is necessary to actively update the state state of App components. At the same time, pass the status modification to LoginPage as props, and LoginPage updates its own state:{visible:true} after the new props is passed. If the customer enters the login information and the login succeeds, You need to set the state of the LoginPage to {visible:false} to hide yourself and call the callback that the App passes to tell the parent attachment that the user has logged in successfully. The LoginPage notifies the App component that the App should be logged in, but it hasn’t refreshed the user’s Feed stream yet. Because the MainPage does not know that the user has logged in, the parent component of App needs to tell it that it has logged in and please refresh. How can it be notified? React is a one-way data flow. In order to make the underlying components update, they can only pass the props property, so there is another overhead of the props property. MainPage updates the associated state and updates the Feed stream itself, which finally complete the display of MainPage information after a login. As can be seen from the above analysis, Poplar has a lot of redundant but unavoidable parameter passing when it transitions from unlogged to logged. Because sibling nodes LoginPage and MainPage cannot simply communicate with each other to inform each other of their status, it needs the bridge of App parent to pass messages up and down first.
Let’s take a look at how the same process is done with Redux:
Poplar already has an initial global login status {status: ‘NOT_LOGGED_IN’} for the application due to the introduction of Redux. This status is updated to {status: ‘LOGGED_IN’}, while LoginPage is bound to this state, Redux will immediately notify the component to update its state to {visible:false}. At the same time, the App is bound to the global state managed by Redux, so it can also get {status: ‘LOGGED_IN’}, so it is easy to hide the LoginPage and display the MainPage after the client has logged in. It is very simple and magic, completely does not rely on the layer of parameter pass, component wants to obtain the global state is associated with it, Redux will notify you first.
2. Implement
To illustrate the react-redux implementation for the next scenario, use actual code as an example:
connect
In App component, UI component is generated into Redux container component through connect method, which can be understood as building a bridge of communication between UI component and Redux and associating store with component.
import {showLoginPage, isLogin} from './actions/loginAction'; import {showNewFeedPage} from './actions/NewFeedAction'; Export default connect((state) => ({status: state.islogin. status, // Login status loginPageVisible: state.showLoginPage.loginPageVisible }), (dispatch) => ({ isLogin: () => dispatch(isLogin()), showLoginPage: () => dispatch(showLoginPage()), showNewFeedPage: () => dispatch(showNewFeedPage()), }))(App)Copy the code
The first argument to the connect method is the mapStateToProps function, which creates a mapping between the data in the store and the PROPS object of the UI component. The mapStateToProps method is called whenever the Store is updated. Is a mapping of UI component props to store data. In the code above, the mapStateToProps accepts state as an argument and returns a mapping between the login state of a UI component and the login state of state in the Store and whether a login page is displayed. The App component state is then associated with Redux’s Store.
The second parameter, mapDispatchToProps, allows action to be bound to a component as props, returning a mapping between the UI component props and the Redux Action. The isLogin showLoginPage showNewFeedPageprops of the App component in the code above maps to the Action of Redux. Call isLogin actually calls store.dispatch(isLogin) action in Redux. Dispatch distributes the action to reducer.
Provider
How does state get passed in connect? React-redux provides a Provider component that lets container components get 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>
)
}
}Copy the code
In the code above, the Provider wraps a layer around the root component so that all child components of the App get state by default.
Action & Reducer
Now that components are associated with the Redux global state, how do you implement the flow of state? How does login status spread across the entire application?
The Actions and Reducer in Redux are required. The actions receive events from the UI components, and the Reducer responds to the actions, returns a new store, and triggers updates of UI components associated with the store.
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();Copy the code
In this login scenario, LoginPage binds its props to store and action, and if the login succeeds, calls showLoginPage(false) Action to hide itself. Reducer received the action from this dispatch and updated the store state:
//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; }}Copy the code
Also call the isLogin action to update the global status of the application to logged in:
//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;
}
}Copy the code
Because the App component has been associated with this global login state, after reducer updated this state, the App will also receive this update and re-render itself, at this point MainPage will be rendered:
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(); }} > // Global status changed from NOT_LOGGED_IN to LOGGED_IN {status == 'NOT_LOGGED_IN'? <LoginPage {... this.props}/>:<MainPage {... this.props}/>}Copy the code
Back-end microservices architecture
Project construction & Development
1. Project structure
Poplar, as an overall Maven project, has no business functionality or code at the top level, and a dual identity for the poM dependency import at the bottom level that provides the basis for poplar-API: Poplar-user-service: a microservice provider that provides registration, login, and user management services Poplar notice-service: a microservice provider that provides notification messaging services
Each subproject is created separately as a Module
2. Maven aggregation project
Poplar consists of multiple service providers, consumers, and common components, and their dependencies are both relational and parent-child dependencies. To simplify configuration and facilitate unified construction, reasonable dependencies need to be established. The service provider is mainly the Spring Boot project, with database access and other dependence; The consumer of the service is also the Spring Boot project, but as it is an API layer, it needs to provide interfaces externally, so it needs to support Controller. The service consumer and provider make calls through Dubbo, which also require a common Dubbo component, so we can see that the consumer and provider rely on Spring Boot and Dubbo together. We can extract a parent POM and define a common parent component:
< the groupId > com. Lvwangbeta < / groupId > < artifactId > poplar < / artifactId > < version > 0.0.1 - the 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>Copy the code
In addition to introducing a common build package, the Poplar parent component also needs to declare its contained child components. The reason for this is that Maven can calculate the dependencies and build order between modules in the reactor when building at the Poplar top level. We introduce service providers and consumers:
<modules>
<module>poplar-common</module>
<module>poplar-api</module>
<module>poplar-feed-service</module>
<module>poplar-user-service</module>
</modules>Copy the code
The poM structure of the child component is much simpler, specifying parent and poM source as the relative path of the parent component
< the groupId > com. Lvwangbeta < / groupId > < artifactId > poplar - API < / artifactId > < version > 0.0.1 - the SNAPSHOT < / version > <packaging>war</packaging> <name>poplar-api</name> <description>poplar api</description> <parent> < the groupId > com. Lvwangbeta < / groupId > < artifactId > poplar < / artifactId > < version > 0.0.1 - the SNAPSHOT < / version > < relativePath >.. /pom.xml</relativePath> <! -- lookup parent from repository --> </parent>Copy the code
There is also a public build package which we did not mention. It mainly contains interfaces shared by consumers, providers, model, Utils methods, etc. It does not rely on Spring and does not require database access. This is a public component which is referenced by other projects. No need to rely on parent:
< the groupId > com. Lvwangbeta < / groupId > < artifactId > poplar - common < / artifactId > < version > 0.0.1 - the SNAPSHOT < / version > <packaging>jar</packaging>Copy the code
When the project is packaged as a whole, Maven calculates that other subprojects that depend on this local JAR package will have priority in the local Maven library. To check the build order, run MVN Clean Install in the Poplar project root directory. You can see that the subprojects are not executed in the order we defined in Poplar- POM. Instead, the Maven reactor calculates the dependencies of each module to execute the build. Build the common dependencies package first, then Poplar, and finally each consumer and provider.
[INFO] Reactor Summary: [INFO] [INFO] poplar-common ……………………………….. SUCCESS [s] 3.341 [INFO] poplar… SUCCESS [s] 3.034 [INFO] poplar – API… SUCCESS [s] 25.028 [INFO] poplar – feed – service… SUCCESS [s] 6.451 [INFO] poplar – user – service… SUCCESS [s] 8.056 [INFO] — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — –
If we modify only a few subprojects, we don’t need a full build, just specify the project with Maven’s -pl option and build its dependent modules with -am. We try to build a separate project called poplar-API, which relies on poplar-common and poplar:
mvn clean install -pl poplar-api -am Copy the code
Maven will build poplar-COMMON and poplar-API first before building itself:
[INFO] Reactor Summary: [INFO] [INFO] poplar-common ……………………………….. SUCCESS [s] 2.536 [INFO] poplar… SUCCESS [s] 1.756 [INFO] poplar – API… SUCCESS [s] 28.101 [INFO] — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — [INFO] BUILD SUCCESS
3. Dubbo & Zookeeper
The service providers and consumers described above rely on Dubbo for remote invocation, but they also need a registry to register service providers and notify service consumers. Zookeeper is one such registry implementation, and Poplar uses Zookeeper as its registry.
3.1 they are installed
Download and decompress the Zookeeper file
$CD Zookeeper -3.4.6 $mkdir dataCopy the code
Creating a Configuration File
$ vim conf/zoo.cfg
tickTime = 2000
dataDir = /path/to/zookeeper/data
clientPort = 2181
initLimit = 5
syncLimit = 2Copy the code
Start the
$ bin/zkServer.sh startCopy the code
stop
$ bin/zkServer.sh stop Copy the code
3.2 Dubbo admin
Dubbo administrative console installation
git clone https://github.com/apache/incubator-dubbo-ops
cd incubator-dubbo-ops && mvn package Copy the code
Decompress the war package to the Tomcat webapps/ROOT directory (the ROOT directory should be emptied in advance). You can view the dubo. properties file that contains the IP address and port of the registry Zookeeper
Dubbo. Registry. Address = zookeeper: / / 127.0.0.1:2181 dubbo. Admin. Root. The password = root dubbo. Admin. Guest. Password = guestCopy the code
Start tomcat
./bin/startup.sh Copy the code
access
http://127.0.0.1:8080/ Copy the code
This completes Dubbo’s monitoring setup for the registry
Development of 4.
Although the development mode of provider and consumer of microservices is different from the previous single architecture application, the logical relationship is almost the same, but the introduction of a registry requires the cooperation of consumers and providers to realize a request, which inevitably requires the negotiation of interfaces and models between the two to ensure the availability of calls.
The documentation uses user registration as an example to show the complete development process from channel invocation to service provider, consumer, and public module publication.
4.1 public
Popular-common, as a common module, defines interfaces and models that both consumers and providers rely on, so that microservices can be accessed to define user service interfaces when they are released
public interface UserService {
String register(String username, String email, String password);
}Copy the code
4.2 Service Providers
UserServiceImpl implements the UserService interface defined in Poplar-Common
@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;
...
}Copy the code
The @service annotation must be included in the Dubbo package, so that Dubbo can scan the Service and complete the registration with 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. The id = dubbo dubbo.protocol.name=dubbo dubbo.protocol.port=9001Copy the code
4.3 Service to Consumers
As mentioned earlier, Poplar-API acts as an API gateway while also serving as a service consumer, organizing providers to invoke relationships and complete request links.
The API layer uses the @Reference annotation to request services from the registry and communicates with the service provider’s RPC through the UserService interface defined in the Popular-Common module
@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; }}Copy the code
Application. The properties configuration
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:2181Copy the code
5. Docker-oriented service
If all the above steps have been completed, a complete microservice architecture has been basically built and coding business code can be started, why do we need to do Docker transformation again? First of all, with the increase of business complexity, new microservice modules may be introduced. It is necessary to provide a stable peripheral environment while developing new modules. If the test environment is not ideal, you can start the necessary Docker containers by yourself to save compilation time. In addition, it reduces the stability of program operation caused by environment migration, facilitates testing and deployment, and provides a more convenient and efficient deployment mode for continuous integration.
All microservice modules included in Poplar can be dockerized and launched with one click by executing build.sh in the poplar root directory:
cd poplar && ./build.shCopy the code
If you have the patience, take a look at the following two small chapters. How does this work
5.1 Building an Image
Poplar uses a separate docker-based deployment model for microservices with databases and registries. Poplar dubbo-Admin is the Dubbo administrative console. Poplar-api poplar-tag-service poplar-action-service poplar-feed-service poplar-user-service is a specific service layer module. Poplar-redis Poplar-mysql provides cache and persistent data support. Poplar-zookeeper is the ZooKeeper registry
poplar-dubbo-admin
poplar-api
poplar-tag-service
poplar-action-service
poplar-feed-service
poplar-user-service
poplar-redis
poplar-mysql
poplar-zookeeperCopy the code
poplar-api poplar-tag-service poplar-action-service poplar-feed-service In the configuration file, you can specify the working directory, basic image, and other information to omit the Dockerfile:
< plugin > < groupId > com. The company < / groupId > < artifactId > docker maven - plugin < / artifactId > < version > 1.0.0 < / version > <configuration> <imageName>lvwangbeta/poplar</imageName> <baseImage>java</baseImage> <maintainer>lvwangbeta [email protected]</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>Copy the code
If you want a subproject not to perform a docker build, you can set the skipDockerBuild of the subproject pom. XML to true.
<skipDockerBuild>true</skipDockerBuild>Copy the code
In the poplar project root directory, run the following command to complete the business layer construction of the entire project:
mvn package -Pdocker -Dmaven.test.skip=true docker:buildCopy the code
[INFO] Building image lvwangbeta/poplar-user-service Step 1/6 : FROM java ---> d23bdf5b1b1b Step 2/6 : MAINTAINER lvwangbeta [email protected] ---> 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/ --> poplardir 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] ------------------------------------------------------------------------
Copy the code
5.2 Starting the Running Container
Because Poplar contains too many containers, we create a custom network for it, poplar-Netwotk
Docker network create --subnet=172.18.0.0/16 poplar-networkCopy the code
The container that runs the image built above and assigns IP addresses on the same network segment to it
Start the Zookeeper registry
Docker run --name poplar-zookeeper --restart always -d --net poplar-network -- IP 172.18.0.6 zooKeeperCopy the code
Start the 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-mysqlCopy the code
Start the Redis
Docker run --net poplar-network -- IP 172.18.0.9 --name poplar-redis -p 6380:6379 -d redisCopy the code
Start business service
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 docker run --net poplar-network -- IP 172.18.0.10 --name=poplar-api -p 8080:8080 -t lvwangbeta/poplar-apiCopy the code
At this point, the back end of the Poplar project is fully built and launched to provide services externally, and clients (whether Web or App) see only one unified API.