前后端分离实践AngularJS/ReactJS/VueJS组件化开发示例与对比(三)——React构建项目示例

说到组件化前端开发框架,React绝对是如今的翘楚,轻量、虚拟DOM、快速响应等等优点深受广大前端开发者青睐。

本实例将使用React构建一个典型企业站的架构和新闻中心模块。

 

一、项目目录结构

该示例最终的项目目录结构如下:

前后端分离实践AngularJS/ReactJS/VueJS组件化开发示例与对比

根据这个目录结构简单描述一下这个示例:package.json文件用于管理包依赖,所有的包依赖都在node_modules文件夹中,webpack.config.js是webpackr的配置文件,主要是定义生成js文件的位置,生成后,会在src文件夹下打包生成main.min.js文件,这个文件就是index.html项目入口文件中需要引入的主要逻辑文件,main.js是React组件渲染,这个文件调用其他组件(其他组件又可以调用其各自的子组件),最后渲染出来。Components文件夹下都是一些子组件,或称为部件,pages文件夹是页面级的组件,如Index.js即主要的页面组件上,它调用Component里面的部件组件,根据一定的需求的逻辑处理拼合并展示。

 

二、开发环境

在这里我使用React + Webpack + webpack-dev-server搭建环境,让开发过程更简便,而且可以实时看到每次修改的效果,加速开发效率。

在这里不详细讲开发环境的详细配置,可以参考《ReactJS + Webpack 开发环境配置》文章进行配置。在这里给出快速搭建可用于开发的实现步骤。

在开始之前,确保你的电脑安装了node.js

1、新建项目文件夹React

2、在React文件夹下新建package.json文件,将以下代码拷贝到该文件中:

{
  "name": "reactnews",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "author": "",
  "license": "ISC",
  "dependencies": {
    "babel-core": "^6.14.0",
    "babel-loader": "^6.2.5",
    "babel-preset-es2015": "^6.14.0",
    "babel-preset-react": "^6.11.1",
    "react": "^15.3.1",
    "react-dom": "^15.3.1"
  },
  "devDependencies": {
    "webpack": "^1.13.2",
    "webpack-dev-server": "^1.15.0"
  },
  "scripts": {
    "server": "webpack-dev-server --host localhost --port 808 --content-base src --inline --hot",
    "build": "webpack --progress -p",
    "test": "echo \"Error: no test specified\" && exit 1"
  }
}

3、在React文件夹下新建webpack.config.js文件,将以下代码拷贝到该文件中:

process.env.NODE_ENV = 'production';
var debug = process.env.NODE_ENV !== "production";
var webpack = require('webpack');
var path = require('path');
module.exports = {
	context: path.join(__dirname, "src"),
	devtool: debug ? "inline-sourcemap" : null,
	entry: "./js/main.js",
	module: {
		loaders: [
			{
				test: /\.jsx?$/,
				exclude: /(node_modules|bower_components)/,
				loader: 'babel-loader',
				query: {
	 				presets: ['react', 'es2015']	
				}
			}
		]
	},
	output: {
	path: __dirname + "/src/",
	filename: "main.min.js"
	},
	plugins: debug ? [] : [
		new webpack.DefinePlugin({
			'process.env': {
				'NODE_ENV': JSON.stringify('production')
			}
		})
	],
};

4、打开命令行窗口

执行npm install -g webpack webpack-dev-server

完成后再执行npm install

 

上面两步完成后,开发环境即搭建完成了。下面就可以开始使用React来构建页面和组件了。

 

三、页面及组件构建

1、部件组件构建

(1)头部组件

头部组件渲染了一个logo和一个搜索条,搜索按钮的响应事件通过在主页面组件调用该头部组件时的属性中绑定,实际响应事件方法在主页面组件中定义,头部组件将搜索关键词文本框内容作为参数传递给事件响应方法。

import React from "react";
export default class Header extends React.Component {
	constructor(props) {
		super(props);

		This.state = {
			keyword: ''
		};
	}

	//搜索功能第二种写法
	// search(keyword){
	// 	window.location.href = encodeURI('index.html?s=' + keyword);
	// }
	
	// <button onClick={()=>this.search(this.refs.keyword.value)}>搜索</button>

	render(){
		return (
			<header>
				<img className="ngLogo" src="img/react.png" />
				<div className="search-wrapper cf">
					<input type="text" ref="keyword"  placeholder="请输入搜索关键词..." />
					<button onClick={()=>this.props.searchNews(this.refs.keyword.value)}>搜索</button>
				</div>
			</header>
		);
	}
}

 

(2)菜单组件

菜单组件渲染一个菜单栏,菜单组件接收两个参数,菜单栏显示数据以及当前的激活菜单项。

import React from "react";
export default class Menu extends React.Component {
	constructor(props) {
		super(props);
	}

	render(){
		return (
			<nav>
				<ul>
					{this.props.menus.map(function(menu, index){
						return <li key={index}><a href={menu.link} className={menu.title == '新闻中心' ? 'active' : ''}>{menu.title}</a></li>
					})
					}
		        </ul>
		    </nav>
		);
	}
}

 

(3)底部组件

底部组件比较简单,不需要数据传递,只渲染静态内容。

import React from "react";
export default class Footer extends React.Component {
	render(){
		return (
			<footer>
				<img className="footerLogo" src="img/react_.png" />
	            <p>ReactJS 组件化开发示例</p>
	        </footer>
		);
	}
}

 

(4)边栏组件及边栏组件下的随机新闻条目组件

边栏部分将使用两个组件来完成,一个是Sidebar边栏组件,作为边栏的父级组件,这是需要在主页面组件Index中引入的,另外,因为边栏部分有随机新闻,随机新闻条目是循环输出的,因此,定义一个边栏新闻条目子组件,方便循环输出。

边栏新闻条目组件只负责随机新闻条目信息(缩略图、标题、日期)的显示,这个组件接收来自边栏组件的新闻条目数据并按指定样式显示。

import React from "react";
export default class SidebarNewsItem extends React.Component {
	constructor(props) {
	  super(props);
	  this.state = {};
	}

	render(){
		var formatDate = function(dateStr){
			var dateObj = new Date(Date.parse(dateStr.replace(/-/g, "/")));
			return dateObj.getFullYear() + '/' + (dateObj.getMonth()+1) + '/' + dateObj.getDate();
		};

		return (
			<div className="newsItem">
				<a href={`index.html?id=${this.props.newscontent.id}`}><img src={this.props.newscontent.featured_img} alt={this.props.newscontent.title} /></a>
                <div>
                	<a href={`index.html?id=${this.props.newscontent.id}`}>
                        <h4>{this.props.newscontent.title}</h4>
                        <h5>{formatDate(this.props.newscontent.create_time)}</h5>
                    </a>
                </div>
            </div>
		);
	}
}

边栏组件主要分为两部分,上面是宣传图片显示,下面是随机新闻显示。随机新闻需要从远程获取,在componentDidMount方法中进行远程获取数据操作(componentDidMount是React组件生命周期的一个方法)。在这里直接使用HTML5的fetch API进行获取,获取到数据后,将数据状态变更,并在渲染方法中循环输出新闻条目,在循环输出时需要调用上面定义的边栏新闻条目组件,因此,在页面开始时需要引入新闻条目组件,并通过属性将单个新闻条目的信息传递给新闻条目组件,由它去做显示。

引入新闻条目组件:

import SidebarNewsItem from ‘./SidebarNewsItem’;

import React from "react";
import SidebarNewsItem from './SidebarNewsItem';

export default class Sidebar extends React.Component {
	constructor(props) {
		super(props);

		this.state = {
			num: 5,
			apiUrl: 'http://localhost:8080',
			news:[]
		};
	}

	componentDidMount(){
		fetch(this.state.apiUrl + '/random/' + this.state.num)
			.then((res) => res.json())
			.then((resJson) => {
				// console.log(resJson);
				if(resJson.status){
					this.setState({news: resJson.data});
				}
			})
	}

	render(){
		return (
			<aside>
	            <img src="img/banner.jpg" />
            	<section className="randomNews">
            		<h3>随机新闻</h3>
                	{
                		this.state.news.map(function(newsitem, index){
							return (
								<SidebarNewsItem key={index} newscontent={newsitem} />
							);
						})
					}
            	</section>
	        </aside>
		);
	}
}

 

(5)新闻列表组件及新闻列表组件下的新闻条目组件

与上面类似的,新闻列表区域也同样需要新闻列表组件和新闻条目组件两个组件。

新闻条目组件的定义与边栏的新闻条目组件定义类似。

import React from "react";
export default class NewsListItem extends React.Component {
	constructor(props) {
	  super(props);
	  this.state = {};
	}

	render(){
		var formatDate = function(dateStr){
			var dateObj = new Date(Date.parse(dateStr.replace(/-/g, "/")));
			return dateObj.getFullYear() + '/' + (dateObj.getMonth()+1) + '/' + dateObj.getDate();
		};

		return (
			<section className="newsItem">
		        <a href={`index.html?id=${this.props.newsinfo.id}`}><img src={this.props.newsinfo.featured_img} alt={this.props.newsinfo.title} /></a>
		        <div>
		        	<a href={`index.html?id=${this.props.newsinfo.id}`}>
			            <h3>{this.props.newsinfo.title}</h3>
			            <h4>{this.props.newsinfo.description}</h4>
			            <h5>{formatDate(this.props.newsinfo.create_time)}</h5>
		            </a>
		        </div>
		    </section>
		);
	}
}

这里的新闻列表组件只是负责渲染数据,获取数据部分放在主页面组件中。新闻列表组件接收一个数组类型的新闻对象列表,并引入新闻列表条目组件,将新闻列表数组中的新闻条目对象循环赋值给新闻条目组件,最终显示新闻列表。

import React from "react";
import NewsListItem from './NewsListItem';

export default class NewsList extends React.Component {
	constructor(props) {
		super(props);
	}

	render(){
		return (
			<article className="newsList">
				{
	        		this.props.news.map(function(newsitem, index){
						return (
							<NewsListItem key={index} newsinfo={newsitem} />
						);
					})
				}
			</article>
		);
	}
}

 

6)新闻内容组件

新闻内容组件接收一个新闻id参数,并根据这个id向远程请求获取指定的新闻信息,最后渲染显示。

import React from "react";

export default class NewsContent extends React.Component {
	constructor(props) {
		super(props);

		this.state = {
			apiUrl: 'http://localhost:8080',
			news: {},
			keyword:[]
		};
	}

	fetchNews(id){
		fetch(this.state.apiUrl + '/news/' + id)
			.then((res) => res.json())
			.then((resJson) => {
				if(resJson.status){
					var kk = resJson.data.keyword.split(',');
					var arr = [];
					for(var index in kk){
						keyword: this.state.keyword.push(kk[index])
					}
					this.setState({news: resJson.data});
				}
			});
	}

	componentWillMount(){
		if(this.props.newsid){
			this.fetchNews(this.props.newsid);
		}
	}

	render(){
		return (
			<div className="newsContent">
				<section>
		            <h3>{this.state.news.title}</h3>
		            <h4>{this.state.news.source}</h4>   <h5>{this.state.news.create_time}</h5>
		            <hr />
		            <div className="content" dangerouslySetInnerHTML={{__html: this.state.news.content}}></div>
		            <div className="keyword">            
		            {
		            	this.state.keyword.map(function(value, index){
		            		return (
		            			<span key={index}>{value}</span>
		            		);
		            	})
		            }
		            </div>
		        </section>
	        </div>
		);
	}
}

至此,所有需要用到的部件组件已全部做完。

 

2、主页面组件构建

主页面组件是这个示例项目中最复杂的一个文件。主页面组件负责拼装整个页面,决定什么时候显示哪些模块或信息。主页面组件主要分成以下几部分:

程序逻辑部分:

(1) 引入React以及需要用到的子组件(部件组件)

import React from "react";
import Header from '../components/Header';
import Menu from '../components/Menu';
import NewsList from '../components/NewsList';
import NewsContent from '../components/NewsContent';
import Sidebar from '../components/Sidebar';
import Footer from '../components/Footer';

(2)定义获取网址参数的函数

var GetQueryString = function(name){
	var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
	var r = decodeURI(window.location.search).substr(1).match(reg);
	if(r!=null)return  unescape(r[2]); return null;
}

(3)初始化状态和数据

constructor(props) {
  	super(props);
  	this.state = {
  		activeMenu: '新闻中心',
  		keyword: GetQueryString('s') ? GetQueryString('s') : null,
  		id: GetQueryString('id') ? GetQueryString('id') : null,
  		page: 1,
		pagesize:5,
		hasMore: true,
		apiUrl: 'http://localhost:8080',
		news:[],
		isSearch: false,
		menus: [
			{link: '#', title:'首页'},
			{link: '#', title:'关于我们'},
			{link: 'index.html', title:'新闻中心'},
			{link: '#', title:'产品中心'},
			{link: '#', title:'服务范围'},
			{link: '#', title:'在线商店'},
			{link: '#', title:'联系我们'}
		]
	};
}

(4)定义获取远程数据的函数,用于分页显示新闻列表

fetchData(page){
	var page = page ? page : 1;
	fetch(this.state.apiUrl + '/news?page=' + page + '&pagesize=' + this.state.pagesize)
		.then((res) => res.json())
		.then((resJson) => {
			if(resJson.status){
				this.setState({news: this.state.news.concat(resJson.data), page: this.state.page+1});
				if(this.state.pagesize > resJson.data.length){
					this.setState({hasMore: false});
				}
			}else{
				this.setState({hasMore: false});
			}
		});
}

(5)定义搜索方法

searchNews(keyword){
	fetch(this.state.apiUrl + '/search?s=' + keyword)
		.then((res) => res.json())
		.then((resJson) => {
			if(resJson.status){
				this.setState({news: resJson.data, hasMore: false, isSearch: true, keyword: keyword});
			}
		});
}

(6)初始化渲染页面,判断当前是否是搜索,是则执行搜索方法,不是则正常调用远程数据显示新闻列表。

componentWillMount(){
	if(this.state.keyword){
		this.searchNews(this.state.keyword);
	}else{
		this.fetchData(this.state.page);
	}
}

 

渲染相关部分:

(1)判断当前是何种状态,分别有正常显示新闻列表状态,显示单条新闻详情状态以及搜索状态,根据当前状态显示不同的组件。

var main;
if(this.state.keyword){
	main = <NewsList news={this.state.news} />
}else{
	if(this.state.id){
		main = <NewsContent newsid={this.state.id} />
	}else{
		main = <NewsList news={this.state.news} />
	}
}

(2)如果是搜索状态,显示新闻搜索信息条

var searchbar;
if(this.state.isSearch){
	searchbar = <section className="newsItem"><p>共为您找到<span>{this.state.news.length}</span>个符合关键词"<span>{this.state.keyword}</span>"的条目</p></section>;
}

(3)判断是否有更多新闻数据可供加载,如果有,则显示“加载更多”链接,如果没有,则不显示

var loadMoreBtn;
if(this.state.hasMore && !this.state.id && !this.state.keyword){
	loadMoreBtn = <div className="loadmore" onClick={() => this.fetchData(this.state.page)}>加载更多</div>
}

if(this.state.id || this.state.keyword){
	loadMoreBtn = null;
}

(4)传入数据,渲染页面

<div className="container">
	<Header searchNews={this.searchNews.bind(this)} />
	<Menu menus={this.state.menus} active={this.state.activeMenu} />
	<div className="main">
		<div className="mainContainer">
			{searchbar}
			{main}
			{loadMoreBtn}
		</div>
        <Sidebar />
    </div>
	<Footer />
</div>

 

主页面组件最终完整代码:

import React from "react";
import Header from '../components/Header';
import Menu from '../components/Menu';
import NewsList from '../components/NewsList';
import NewsContent from '../components/NewsContent';
import Sidebar from '../components/Sidebar';
import Footer from '../components/Footer';

var GetQueryString = function(name){
	var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
	var r = decodeURI(window.location.search).substr(1).match(reg);
	if(r!=null)return  unescape(r[2]); return null;
}
		
export default class Index extends React.Component {
	constructor(props) {
	  	super(props);
	  	this.state = {
	  		activeMenu: '新闻中心',
	  		keyword: GetQueryString('s') ? GetQueryString('s') : null,
	  		id: GetQueryString('id') ? GetQueryString('id') : null,
	  		page: 1,
			pagesize:5,
			hasMore: true,
			apiUrl: 'http://localhost:8080',
			news:[],
			isSearch: false,
			menus: [
				{link: '#', title:'首页'},
				{link: '#', title:'关于我们'},
				{link: 'index.html', title:'新闻中心'},
				{link: '#', title:'产品中心'},
				{link: '#', title:'服务范围'},
				{link: '#', title:'在线商店'},
				{link: '#', title:'联系我们'}
			]
		};
	}

	fetchData(page){
		var page = page ? page : 1;
		fetch(this.state.apiUrl + '/news?page=' + page + '&pagesize=' + this.state.pagesize)
			.then((res) => res.json())
			.then((resJson) => {
				if(resJson.status){
					this.setState({news: this.state.news.concat(resJson.data), page: this.state.page+1});
					if(this.state.pagesize > resJson.data.length){
						this.setState({hasMore: false});
					}
				}else{
					this.setState({hasMore: false});
				}
			});
	}

	searchNews(keyword){
		fetch(this.state.apiUrl + '/search?s=' + keyword)
			.then((res) => res.json())
			.then((resJson) => {
				if(resJson.status){
					this.setState({news: resJson.data, hasMore: false, isSearch: true, keyword: keyword});
				}
			});
	}

	componentWillMount(){
		if(this.state.keyword){
			this.searchNews(this.state.keyword);
		}else{
			this.fetchData(this.state.page);
		}
	}


	render(){
		var main;
		if(this.state.keyword){
			main = <NewsList news={this.state.news} />
		}else{
			if(this.state.id){
				main = <NewsContent newsid={this.state.id} />
			}else{
				main = <NewsList news={this.state.news} />
			}
		}

		var searchbar;
		if(this.state.isSearch){
			searchbar = <section className="newsItem"><p>共为您找到<span>{this.state.news.length}</span>个符合关键词"<span>{this.state.keyword}</span>"的条目</p></section>;
		}

		var loadMoreBtn;
		if(this.state.hasMore && !this.state.id && !this.state.keyword){
			loadMoreBtn = <div className="loadmore" onClick={() => this.fetchData(this.state.page)}>加载更多</div>
		}

		if(this.state.id || this.state.keyword){
			loadMoreBtn = null;
		}

		return (
			<div className="container">
				<Header searchNews={this.searchNews.bind(this)} />
				<Menu menus={this.state.menus} active={this.state.activeMenu} />
				<div className="main">
					<div className="mainContainer">
						{searchbar}
						{main}
						{loadMoreBtn}
					</div>
	                <Sidebar />
	            </div>
				<Footer />
			</div>
		);
	}
}

 

3、渲染页面

import React from "react";
import ReactDOM from "react-dom";
import Index from './pages/Index';

const app = document.getElementById('app');

ReactDOM.render(
  <Index />,
  app
);

渲染页面文件主要引入主页面组件,指定组件渲染位置,并对主页面组件进行渲染输出。

 

4、页面入口文件

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
	<title>React</title>
	<meta name="description" content="">
    <meta name="viewport" content="width=device-width, initial-scale=1">
	<link rel="stylesheet" href="css/main.css">
</head>
<body>
	<div class="container">
		<div id="app"></div>
	</div>

<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20src%3D%22main.min.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<script>" title="<script>" />
</body>
</html>

页面入口文件只需要引入生成的main.min.js文件和渲染页面组件时指定的标签即可。

main.min.js的生成非常简单,打开命令行工具,进行项目根目录,执行

npm run build

等待执行完成后,就会在src目录下生成main.min.js文件。

 

5、样式文件

body{background-color: #f8f8f8;margin: 0;}
.container{margin:0 auto; width:1000px;}

img{display: block;}
a{text-decoration: none;}

/************************************************************/
/************************************************************/
/************************************************************/
/************************************************************/
/********************* 添加新闻资讯 *************************/
/************************************************************/
/************************************************************/
/************************************************************/
/************************************************************/

h1{width:100%; height: 90px; background-color: #dd3333; color:#fff; line-height: 90px; text-align: center; margin:0;font-family: '微软雅黑'; font-weight: normal; margin-bottom: 20px;}
.pure-control-group{display: flex; justify-content: center; align-items: center;}
.pure-form-aligned .pure-control-group label{width:4rem;}
.pure-control-group input{flex:0.96;}
.pure-control-group textarea{flex:0.96;}
.richTextEditor{flex:0.92;border:1px solid #eee;}
.pure-form button{margin: 0 auto;}

.news-content{padding:20px;}
trix-editor{min-height: 200px !important;}

/************************************************************/
/************************************************************/
/************************************************************/
/************************************************************/
/********************* 前台页面布局 *************************/
/************************************************************/
/************************************************************/
/************************************************************/
/************************************************************/

.main{display: flex;flex-direction: row; justify-content: space-between;margin-bottom: 10px;}
header{width:100%;overflow: hidden;height: 100px;display: flex; justify-content: space-between; align-items: center;}
nav{width:100%;overflow: hidden;height: 50px;background-color: #444; margin:10px 0 20px 0;}
article{flex:1;min-height: 600px;}
aside{width:300px; margin-left: 10px;}
footer{width:100%;overflow: hidden;height: 150px;background-color: #444;display: flex;justify-content: center; align-items: center;flex-direction: column;}

header .ngLogo{width:225px;margin-left: 10px;}
nav ul{margin:0;display: flex;flex-direction: row;padding:0;padding-left: 50px; padding-right: 50px;}
nav li{list-style: none;display: inline-block;flex:1;}
nav li a{display:block;height:50px; line-height:50px;color:#fff;flex:1; text-decoration: none;text-align: center;}
nav li a:hover{background-color: #dd3333;}
nav li .active{background-color: #dd3333;}

footer .footerLogo{display: block;width:200px;height: auto;}
footer p{text-align: center; color:#ddd;}

/*搜索框*/
.cf:before, .cf:after{content:""; display:table;}
.cf:after{clear:both;}
.cf{zoom:1;}    
 /* Form wrapper styling */
.search-wrapper {width: 280px;	margin-right: 10px;	box-shadow: 0 1px 1px rgba(0, 0, 0, .4) inset, 0 1px 0 rgba(255, 255, 255, .2);}
/* Form text input */
.search-wrapper input {width: 198px;height: 20px;padding: 10px 5px;	float: left;font: bold 15px 'lucida sans', 'trebuchet MS', 'Tahoma';border: 0;background: #444;	color:#fff;	border-radius: 3px 0 0 3px;}
.search-wrapper input:focus {outline: 0;background: #444;box-shadow: 0 0 2px rgba(0,0,0,.8) inset;}
.search-wrapper input::-webkit-input-placeholder {color: #eee;font-weight: normal;font-style: italic;}
.search-wrapper input:-moz-placeholder {color: #eee;font-weight: normal; font-style: italic;}
.search-wrapper input:-ms-input-placeholder {color: #eee;font-weight: normal; font-style: italic;}    
/* Form submit button */
.search-wrapper button {overflow: visible;position: relative;float: right;border: 0;padding: 0;cursor: pointer;	height: 40px;width: 72px;font: bold 15px/40px 'lucida sans', 'trebuchet MS', 'Tahoma';	color: white;text-transform: uppercase;	background: #D83C3C;border-radius: 0 3px 3px 0;text-shadow: 0 -1px 0 rgba(0, 0, 0, .3);}
.search-wrapper button:hover{background: #e54040;}   
.search-wrapper button:active,.search-wrapper button:focus{background: #c42f2f; outline: 0;}
.search-wrapper button:before {content: ''; position: absolute; border-width: 8px 8px 8px 0;border-style: solid solid solid none;border-color: transparent #d83c3c transparent;top: 12px;left: -6px;}
.search-wrapper button:hover:before{border-right-color: #e54040;}
.search-wrapper button:focus:before,.search-wrapper button:active:before{border-right-color: #c42f2f;}      
.search-wrapper button::-moz-focus-inner {border: 0; padding: 0;}    

.newsList{flex:1;}
.newsList .newsItem{display: flex;flex-direction: row;padding:10px; border-bottom:1px solid #eee;}
.newsList .newsItem img{width: 220px; height: 122px; margin-right: 16px;}
.newsList .newsItem div{flex:1; display: flex;flex-direction: column; padding-right: 10px;}
.newsList h3,.newsList h4, .newsList h5{margin:0; padding:0; font-weight: normal;}
.newsList h3{font-family: '微软雅黑'; color:#333;max-height:52px;text-overflow: -o-ellipsis-lastline;overflow: hidden;text-overflow: ellipsis;display: -webkit-box;-webkit-line-clamp: 2;-webkit-box-orient: vertical;}
.newsList .newsItem h4{margin:10px 0;color:#666;font-size:14px;height:32px;text-overflow: -o-ellipsis-lastline;overflow: hidden;text-overflow: ellipsis;display: -webkit-box;-webkit-line-clamp: 2;-webkit-box-orient: vertical;}
.newsList .newsItem h5{font-size: 12px;color:#666;}
.newsItem span{color:#dd3333; font-weight: 600;}
.newsList a, .newsList a:visited{color:#444;}
.newsList a:hover{color:#dd3333 !important;}

.loadmore{clear:both;text-align: center;height:40px;line-height: 40px;margin-top: 10px;cursor: pointer; background-color: #eee;}

.mainContainer{flex:1; width:100%;}

.newsContent{flex:1;padding-right: 30px;margin-bottom: 2rem;}
.newsContent h3 {font-size: 30px;}
.newsContent h4, .newsContent h5{display: inline;font-weight: normal;margin-right: 10px;}
.newsContent hr{ height:0px; border-top:1px solid #ddd; border-right:0px; border-bottom:0px; border-left:0px; }
.newsContent .content{margin-top: 2rem;margin-bottom: 2rem;text-justify : auto; text-align:justify;}
.newsContent .keyword span{margin-right:10px; background-color: #dd3333; color: #fff; padding:6px 10px; text-align: center; border-radius: 3px; line-height: 100%;}

.randomNews{width:100%;margin-top: 10px; border:1px solid #eee;}
.randomNews .newsItem{display: flex; padding:10px 0 10px 10px; border-bottom: 1px solid #eee;}
.randomNews img{width: 80px; height: 45px; border-radius: 4px; margin-right: 10px;}
.randomNews div{flex:1;flex-direction: row;padding-right: 10px;}
.randomNews h3,.randomNews h4, .randomNews h5{margin:0; padding:0; font-weight: normal;}
.randomNews h3{font-family: '微软雅黑'; background-color: #444;color:#fff;padding:6px 10px;}
.randomNews h4{font-size: 16px;}
.randomNews h5{color:#444;}
.randomNews a, .randomNews a:visited{color:#444;}

至此,已完成了示例项目的React版页面构建,特别需要注意是:

1、这个示例使用了ES6(ES2015)语法;

2、class是关键字,因此样式类要使用className代替class

 

四、运行效果

前后端分离实践AngularJS/ReactJS/VueJS组件化开发示例与对比

前后端分离实践AngularJS/ReactJS/VueJS组件化开发示例与对比

前后端分离实践AngularJS/ReactJS/VueJS组件化开发示例与对比

 

 

那时那我

jinyunblogadmin

2 thoughts to “前后端分离实践AngularJS/ReactJS/VueJS组件化开发示例与对比(三)——React构建项目示例”

发表评论

电子邮件地址不会被公开。 必填项已用*标注