├── .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 | 
62 |
63 | ### 项目预览
64 | 
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 |
51 |
52 |
53 | )
54 | }
55 | }
56 |
57 | const About = () =>(
58 |
59 |
60 |
About
61 |
62 | )
63 |
64 | const Topic = ( {match} ) =>(
65 |
66 |
67 |
Topic
68 |
69 | - react
70 | - vue
71 | - angular
72 |
73 |
74 |
(
75 | {JSON.stringify({match})}
76 | )}/>
77 |
78 | )
79 |
80 | const Detail = ( {match} ) => (
81 |
82 |
83 |
{match.params.topicId}
84 |
{JSON.stringify({match})}
85 |
86 | )
87 |
88 | const NoMatch = () =>(
89 |
90 |
91 |
404 Not Found
92 |
93 | )
94 |
95 | const Nav = () =>(
96 |
97 |
98 |
99 | - home
100 | - about
101 | - topic
102 | - somewhere
103 |
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 |
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 |
--------------------------------------------------------------------------------