├── .babelrc ├── README.md ├── code.jpg ├── flex.html ├── index.html ├── index.js ├── package.json ├── react.gif ├── server.js ├── src ├── App.css ├── App.js └── component │ └── test.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react",'stage-0'], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React + React-Router4 + nodejs + spa + es6 + flex 新手教程 2 | 3 | React + React-Router4 + es6 + nodejs + flex布局 重写 react-china 社区. 4 | 5 | * 由于技术更新飞快,社区很多教程都已经是过时的技术.仅有的一些可能过于复杂,并不适合新手学习. 6 | * 本教程是专门针对新手的入门教程.本教程会使用2017年4月10号为止,最新版本的react和相关依赖进行开发. 7 | 手把手教大家把一个项目从0到1搭建起来,同时整理了react相关的学习资料. 8 | * 为了方便大家学习,降低学习难度,项目并没有使用redux. 9 | 10 | 11 | ### React基础 12 | * react基本语法学习  http://www.ruanyifeng.com/blog/2015/03/react.html   13 | * react书籍推荐 https://book.douban.com/subject/26918038/ 14 | * react-router语法 https://reacttraining.com/react-router/web/guides/quick-start 15 | * react-router中文 http://blog.csdn.net/sinat_17775997/article/details/69218382 16 | 17 | 18 | 19 | ### JS基础 20 | * es6语法学习 http://es6.ruanyifeng.com/ 21 | * nodejs基础 http://www.nodebeginner.org/index-zh-cn.html 22 | * h5 History API https://segmentfault.com/a/1190000007238999 23 | 24 | 25 | 26 | ### 什么是SPA 27 | 单页 Web 应用 (single-page application 简称为 SPA) 是一种特殊的 Web 应用。它将所有的活动局限于一个Web页面中,仅在该Web页面初始化时加载相应的HTML、JavaScript 和 CSS。一旦页面加载完成了,SPA不会因为用户的操作而进行页面的重新加载或跳转。而是利用 JavaScript 动态的变换HTML的内(采用的是div切换显示和隐藏),从而实现UI与用户的交互。由于避免了页面的重新加载,SPA 可以提供较为流畅的用户体验。得益于ajax,我们可以实现无跳转刷新,又多亏了浏览器的histroy机制,我们用hash的变化从而可以实现推动界面变化。 28 | 29 | 30 | 31 | ### 什么是前端路由 32 | 在web开发中,'route'是指根据url分配到对应的处理程序。 33 | 34 | 35 | 36 | ### 什么是react-router 37 | SPA应用由于只有一个页面,无法很好的处理页面的前进,后退,书签管理等功能.这时候就需要借助react-router来进行页面跳转和管理 38 | 39 | ### 本项目实现的功能 40 | * webpack搭建react开发环境,热加载等功能 41 | * nodejs爬取react-china接口数据,返回给前台 42 | * react + flex布局实现前端界面UI 43 | * 用fetch实现react的数据获取 44 | * 用react-router 实现路由切换 45 | * 滚动条下拉自动获取下一页内容,并重新渲染 46 | 47 | 48 | 49 | ### 项目搭建 50 | ```bash 51 | git clone git@github.com:fjmhzyh/react-china.git     // 将项目下载到本地 52 | $ npm install       // 安装依赖 53 | $ npm start         // 启动项目 54 | ``` 55 | * http://localhost:3000   // 打开项目主页 56 | 57 | ### 你的支持,我的动力 58 | * 如果觉得有帮助的话,请作者喝杯咖啡吧! 59 | * 感谢大家的支持,项目会继续完善,其他教程也会提交到github,欢迎关注! 60 | 61 | ![image](https://github.com/fjmhzyh/react-china/blob/master/code.jpg) 62 | 63 | ### 项目预览 64 | ![image](https://github.com/fjmhzyh/react-china/blob/master/react.gif) 65 | 66 | -------------------------------------------------------------------------------- /code.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fjmhzyh/react-china/ce0d5673abcbf7f3da94fb54811dcb074d8f549a/code.jpg -------------------------------------------------------------------------------- /flex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | flex 7 | 8 | 14 | 15 | 16 |
17 |
999999999999999999999999
18 |
19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './src/App'; 4 | 5 | 6 | render(App(), document.querySelector('#root')); 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-china", 3 | "version": "0.0.0", 4 | "description": "React Redux example", 5 | "scripts": { 6 | "start": "node server.js" 7 | }, 8 | "dependencies": { 9 | "moment": "2.18.1", 10 | "prop-types": "15.5.7", 11 | "react": "^15.5.3", 12 | "react-dom": "^15.5.3", 13 | "react-router-dom": "4.1.0", 14 | "request": "2.81.0", 15 | "whatwg-fetch": "2.0.3" 16 | }, 17 | "devDependencies": { 18 | "babel-core": "^6.24.1", 19 | "babel-loader": "^6.4.1", 20 | "babel-polyfill": "^6.23.0", 21 | "babel-preset-es2015": "^6.24.1", 22 | "babel-preset-react": "^6.24.1", 23 | "babel-preset-react-hmre": "^1.1.1", 24 | "babel-preset-stage-0": "^6.24.1", 25 | "css-loader": "0.28.0", 26 | "express": "^4.15.2", 27 | "style-loader": "0.16.1", 28 | "webpack": "^2.3.3", 29 | "webpack-dev-middleware": "^1.10.1", 30 | "webpack-hot-middleware": "^2.18.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /react.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fjmhzyh/react-china/ce0d5673abcbf7f3da94fb54811dcb074d8f549a/react.gif -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var webpack = require('webpack'); 3 | var webpackDevMiddleware = require('webpack-dev-middleware'); 4 | var webpackHotMiddleware = require('webpack-hot-middleware'); 5 | var config = require('./webpack.config'); 6 | 7 | var app = new (require('express'))(); 8 | var port = 3000; 9 | 10 | var compiler = webpack(config); 11 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); 12 | app.use(webpackHotMiddleware(compiler)); 13 | 14 | app.get("/", function(req, res) { 15 | res.sendFile(__dirname + '/index.html') 16 | }); 17 | 18 | app.listen(port, function(error) { 19 | if (error) { 20 | console.error(error) 21 | } else { 22 | console.info('server has started on port ',port) 23 | } 24 | }); 25 | 26 | 27 | 28 | // 数据接口 29 | var request = require('request'); 30 | var pageSize = 1; 31 | var options = { 32 | url:"http://react-china.org/latest?no_definitions=true&page="+pageSize+"&_=1492065010030", 33 | headers: { 34 | "Accept":"application/json, text/javascript, */*; q=0.01", 35 | "Accept-Encoding":"gzip, deflate, sdch", 36 | "Accept-Language":"zh-CN,zh;q=0.8", 37 | "Connection":"keep-alive", 38 | "Cookie":"__utmt=1; _forum_session=WWdZQTQra2FiWTl4RndWSWNtMkp1UTYzdHIvMGtGQ0NhWXBDQkNoZkplQXpqeHZQcmd1QXBjcEoyY201SUFIdDY4NHFSSWlwOUZTTWthYUhqRk0xcFE9PS0tZlE0cm5EdE9yd280c0dtU2hTV2g3Zz09--95d83fb4504573db12bdbf04715da77b4a4a324d; _ga=GA1.2.837246515.1491988548; __utma=93274398.837246515.1491988548.1492054092.1492064864.4; __utmb=93274398.4.10.1492064864; __utmc=93274398; __utmz=93274398.1492054092.3.3.utmcsr=baidu|utmccn=(organic)|utmcmd=organic", 39 | "Discourse-Track-View":true, 40 | "Host":"react-china.org", 41 | "Referer":"http://react-china.org/latest?no_definitions=true&page=1&_=14", 42 | "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", 43 | "X-CSRF-Token":"undefined", 44 | "X-Requested-With":"XMLHttpRequest" 45 | } 46 | }; 47 | 48 | 49 | app.get('/api/data/:pageSize',function(req,res){ 50 | pageSize = req.params.pageSize; 51 | options.url ="http://react-china.org/latest?no_definitions=true&page="+pageSize+"&_=1492065010030"; 52 | request.get(options,function(error, response, body) { 53 | if(error){ 54 | console.log(error); 55 | } 56 | if (!error && response.statusCode == 200) { 57 | pageSize++; 58 | res.setHeader("Content-Type","application/json;charset=utf-8"); 59 | res.end(body); 60 | } 61 | }) 62 | }) 63 | 64 | 65 | app.get('/api/data/page/:id',function(req,res){ 66 | var id = req.params.id; 67 | options.url ="http://react-china.org/t/"+id+".json?track_visit=true&forceLoad=true&_=1492395696504"; 68 | console.log(options.url) 69 | request.get(options,function(error, response, body) { 70 | if(error){ 71 | console.log(error); 72 | } 73 | if (!error && response.statusCode == 200) { 74 | res.setHeader("Content-Type","application/json;charset=utf-8"); 75 | console.log(body); 76 | res.end(body); 77 | } 78 | }) 79 | }) 80 | 81 | 82 | 83 | var num = 0; 84 | 85 | app.get('/api/test',function(req,res){ 86 | res.write(num.toString()); 87 | num++; 88 | res.end(); 89 | }) -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body{ 2 | margin:0; 3 | padding:0; 4 | overflow:auto 5 | } 6 | 7 | ul,li{ 8 | margin:0; 9 | padding:0; 10 | list-style: none; 11 | } 12 | 13 | a{ 14 | text-decoration: none; 15 | color:#333; 16 | } 17 | 18 | .top-nav{ 19 | position: fixed; 20 | display: -webkit-flex; 21 | display: flex; 22 | backface-visibility:hidden; 23 | left: 0; 24 | top: 0; 25 | width: 100%; 26 | height:60px; 27 | line-height: 60px; 28 | color:#38f; 29 | box-shadow: 0 2px 4px -1px rgba(0,0,0,0.25); 30 | margin-top: 0; 31 | background: #fff 32 | } 33 | 34 | .top-nav-left{ 35 | flex:0 0 300px; 36 | } 37 | 38 | .top-nav-left img{ 39 | margin:0 10%; 40 | max-height: 60px; 41 | } 42 | 43 | .top-nav-right{ 44 | flex: 1 1 auto; 45 | display: flex; 46 | align-items: center; 47 | } 48 | 49 | .top-nav-title{ 50 | color:#333; 51 | font-weight: 600; 52 | } 53 | 54 | .github{ 55 | color:#61dafb; 56 | } 57 | 58 | 59 | .fb-nav{ 60 | display: -webkit-flex; 61 | display: flex; 62 | margin-top: 100px; 63 | } 64 | 65 | 66 | .fb-nav-list{ 67 | flex:1; 68 | text-align: center; 69 | } 70 | 71 | .fb-nav>li>a{ 72 | padding:5px; 73 | } 74 | 75 | .active{ 76 | font-weight: 900; 77 | color:#38f; 78 | border-bottom:2px solid #38f 79 | } 80 | 81 | .msg-list>li{ 82 | padding:0 5%; 83 | border-bottom:1px solid #e9e9e9; 84 | } 85 | 86 | .msg-list>li span{ 87 | margin-left:50px; 88 | } 89 | 90 | .list-ul{ 91 | display: -webkit-flex; 92 | display: flex; 93 | width: 100% 94 | } 95 | 96 | .list-title{ 97 | flex:1 1 auto; 98 | padding:10px 0; 99 | text-align: left; 100 | overflow: hidden; 101 | } 102 | 103 | .list-title a{ 104 | width: 100%; 105 | overflow: hidden; 106 | white-space:nowrap; 107 | } 108 | 109 | .list-other{ 110 | flex:0 0 120px; 111 | padding:10px 0; 112 | text-align:center; 113 | } 114 | 115 | 116 | .item-list:first-child{ 117 | border-bottom:3px solid #e9e9e9; 118 | } 119 | 120 | .item-list:first-child a{ 121 | color:#38f; 122 | } 123 | 124 | 125 | .page-detail{ 126 | margin-top: 80px; 127 | padding: 30px; 128 | } 129 | 130 | 131 | .page-content-box{ 132 | display: -webkit-flex; 133 | display: flex; 134 | border-top: 1px solid #e9e9e9; 135 | } 136 | 137 | .page-content-avatar{ 138 | flex:0 0 80px; 139 | padding: 10px 0 0 0; 140 | } 141 | 142 | .page-content-avatar img{ 143 | width: 45px; 144 | max-width: 45px; 145 | } 146 | 147 | .page-content-right{ 148 | flex:1 1 auto; 149 | padding: 10px 0 0 0; 150 | } 151 | 152 | 153 | .page-comment{ 154 | border-top: 1px solid #e9e9e9; 155 | padding: 10px 0; 156 | } 157 | 158 | .comment-content{ 159 | color:#38f; 160 | } 161 | 162 | .comment-time{ 163 | float:right; 164 | color:#000; 165 | } 166 | 167 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { 4 | BrowserRouter as Router, 5 | Route, 6 | Link, 7 | NavLink, 8 | Switch 9 | } from 'react-router-dom'; 10 | 11 | import './App.css'; 12 | import MessageList from './component/test.js' 13 | 14 | const url = "http://localhost:3000/"; 15 | 16 | class Home extends Component{ 17 | constructor(props) { 18 | super(props); 19 | this.state={ 20 | pageSize:1 21 | } 22 | var me = this; 23 | this.loadNextPage = function() { 24 | let style = document.body.currentStyle?document.body.currentStyle:window.getComputedStyle(document.body,null) 25 | let bh = parseFloat(style.height); // body 高度 26 | let wh = window.innerHeight; // 可视区域高度 27 | let st = document.body.scrollTop; // 滚动距离 28 | var distance = bh - (wh + st) 29 | //console.log(distance); 30 | if( distance < 150){ 31 | window.onscroll = null; 32 | let p = me.state.pageSize+1 33 | me.setState({ 34 | pageSize:p 35 | }) 36 | console.log('set:',me.state.pageSize) 37 | } 38 | } 39 | } 40 | componentDidMount() { 41 | window.onscroll = this.loadNextPage 42 | } 43 | componentDidUpdate(prevProps, prevState) { 44 | console.log('DidUpdate') 45 | window.onscroll = this.loadNextPage 46 | } 47 | render() { 48 | return( 49 |
50 |
53 | ) 54 | } 55 | } 56 | 57 | const About = () =>( 58 |
59 |
62 | ) 63 | 64 | const Topic = ( {match} ) =>( 65 |
66 |
78 | ) 79 | 80 | const Detail = ( {match} ) => ( 81 |
82 |
86 | ) 87 | 88 | const NoMatch = () =>( 89 |
90 |
93 | ) 94 | 95 | const Nav = () =>( 96 |
97 | 98 | 104 |
105 |
106 | ) 107 | 108 | class Top extends Component{ 109 | constructor(props) { 110 | super(props); 111 | this.changeTitle = function(){ 112 | let st = document.body.scrollTop; // 滚动距离 113 | if(st>100){ 114 | let mySpan = this.refs.mySpan; 115 | console.log(top); 116 | mySpan.innerText = this.props.title; 117 | window.onscroll = this.changeBack; 118 | } 119 | }.bind(this); 120 | this.changeBack = function() { 121 | let st = document.body.scrollTop; 122 | if(st<100){ 123 | let mySpan = this.refs.mySpan; 124 | mySpan.innerHTML = '点 这里 ,给我的github一个star吧!'; 125 | window.onscroll = this.changeTitle; 126 | } 127 | }.bind(this); 128 | } 129 | componentDidMount() { 130 | window.onscroll = this.changeTitle; 131 | } 132 | render() { 133 | return ( 134 |
135 |
136 | 137 |
138 |
139 |

140 |
141 |
142 | ) 143 | } 144 | } 145 | 146 | class Page extends Component{ 147 | constructor(props) { 148 | super(props); 149 | this.state = { 150 | data:{ 151 | cooked:'

数据获取中

' 152 | }, 153 | title:'', 154 | comments:[] 155 | } 156 | let me = this; 157 | this.fetchPage = function(id){ 158 | fetch(url+'api/data/page/'+id) 159 | .then(function(response) { 160 | return response.json() 161 | }).then(function(json) { 162 | json.post_stream.posts.forEach( (item,i) =>{ 163 | let url = item.avatar_template.replace(/\{size\}/g,45); 164 | item.avatar_url = '//reactchina.sxlcdn.com'+url; 165 | item.created_at = item.created_at.split('T')[0]; 166 | }) 167 | let data = json.post_stream.posts[0]; 168 | let comments = json.post_stream.posts.splice(1) 169 | me.setState({ 170 | data:data, 171 | title:json.title, 172 | comments:comments 173 | }) 174 | console.log('title:',json.title) 175 | console.log('page:',id) 176 | }).catch(function(ex) { 177 | console.log('parsing failed', ex) 178 | }) 179 | }; 180 | } 181 | componentDidMount() { 182 | console.log('did mount') 183 | let id = this.props.match.params.id; 184 | this.fetchPage(id); 185 | } 186 | render() { 187 | var data = this.state.data; 188 | var comments = this.state.comments; 189 | return ( 190 |
191 | 192 |
193 |

{this.state.title}

194 |
195 |
196 | 197 |
198 |
199 |
200 | { 201 | comments.map( (item,i) =>( 202 |
203 |
204 | 205 |
206 |
207 |
{item.name}{item.created_at}
208 |
209 |
210 |
211 | )) 212 | } 213 |
214 |
215 | ) 216 | } 217 | } 218 | 219 | const App = () =>( 220 | 221 |
222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 |
230 |
231 | ) 232 | 233 | export default App 234 | 235 | 236 | 237 | -------------------------------------------------------------------------------- /src/component/test.js: -------------------------------------------------------------------------------- 1 | import React,{Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | BrowserRouter as Router, 5 | Route, 6 | Link, 7 | NavLink, 8 | Switch 9 | } from 'react-router-dom'; 10 | 11 | import 'whatwg-fetch'; 12 | import moment from 'moment' 13 | moment.lang('zh-cn'); 14 | 15 | const url = "http://localhost:3000/"; 16 | 17 | 18 | const Message = (props) =>( 19 |
  • 20 | 26 |
  • 27 | ) 28 | 29 | class MessageList extends Component{ 30 | constructor(props) { 31 | super(props); 32 | this.state = { 33 | data:[], 34 | pageSize:this.props.pageSize 35 | } 36 | var me = this; 37 | this.fetchData=function(num){ 38 | fetch(url+'api/data/'+num) 39 | .then(function(response) { 40 | return response.json() 41 | }).then(function(json) { 42 | let data = json.topic_list.topics; 43 | data.forEach((item,index) =>{ 44 | switch(item.category_id){ 45 | case 1: 46 | item.category = '提问'; 47 | break; 48 | case 15: 49 | item.category = '分享'; 50 | break; 51 | default: 52 | item.category = '其他'; 53 | break; 54 | } 55 | let time = moment(item.created_at).format("YYYY-MM-DD"); 56 | item.created_at = moment(time, "YYYY-MM-DD").fromNow(); 57 | }) 58 | let newData = me.state.data.concat(data) 59 | me.setState({ 60 | data:newData 61 | }) 62 | console.log('fetch:',num) 63 | }).catch(function(ex) { 64 | console.log('parsing failed', ex) 65 | }) 66 | } 67 | } 68 | componentDidMount() { 69 | this.fetchData(this.props.pageSize); 70 | //window.onscroll = this.loadNextPage 71 | } 72 | componentWillReceiveProps(nextProps) { 73 | this.fetchData(nextProps.pageSize); 74 | } 75 | shouldComponentUpdate(nextProps, nextState) { 76 | //alert("should component up data?") 77 | return true 78 | } 79 | componentDidUpdate(prevProps, prevState) { 80 | //this.fetchData(this.state.pageSize); 81 | } 82 | render() { 83 | if(this.state.data.length>0){ 84 | var list = this.state.data.map( (item,i) => ( 85 | 86 | )) 87 | return ( 88 |
      89 | 90 | {list} 91 |
    92 | ) 93 | }else{ 94 | return ( 95 |

    数据加载中

    96 | ) 97 | } 98 | } 99 | } 100 | 101 | export default MessageList; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | './index.js' 9 | ], 10 | output: { 11 | path: path.join(__dirname, '/dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/static/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurrenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin() 18 | ], 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.js$/, 23 | exclude: /node_modules/, 24 | loader: 'babel-loader?presets[]=es2015&presets[]=react&presets[]=stage-0' 25 | }, 26 | { 27 | test: /\.css$/, 28 | exclude: /node_modules/, 29 | loader: "style-loader!css-loader" 30 | } 31 | ] 32 | } 33 | }; 34 | --------------------------------------------------------------------------------