├── public
├── favicon.ico
├── manifest.json
└── index.html
├── src
├── index.css
├── App.test.js
├── util
│ ├── isMobile.js
│ └── getData.js
├── index.js
├── App.css
├── components
│ ├── page
│ │ ├── AboutPage.js
│ │ ├── ArticleListPage.js
│ │ ├── ArticlePage.js
│ │ └── NodePage.js
│ └── common
│ │ ├── Comment.js
│ │ └── Article.js
├── App.js
└── registerServiceWorker.js
├── .gitignore
├── config
├── jest
│ ├── fileTransform.js
│ └── cssTransform.js
├── polyfills.js
├── paths.js
├── env.js
├── webpackDevServer.config.js
├── webpack.config.dev.js
└── webpack.config.prod.js
├── scripts
├── test.js
├── start.js
└── build.js
├── README.md
└── package.json
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bergwhite/v2ex-react/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | });
9 |
--------------------------------------------------------------------------------
/src/util/isMobile.js:
--------------------------------------------------------------------------------
1 | function isMobile() {
2 | const checkList = ['Android', 'iPhone', 'SymbianOS', 'Windows Phone', 'iPad', 'iPod']
3 | const result = checkList.filter((e) => {
4 | return navigator.userAgent.indexOf(e) !== -1
5 | })
6 | return result.length !== 0
7 | }
8 |
9 |
10 | export default isMobile;
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "V2EX-React",
3 | "name": "React重构V2EX社区",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 |
11 | # misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
--------------------------------------------------------------------------------
/config/jest/fileTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 |
5 | // This is a custom Jest transformer turning file imports into filenames.
6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html
7 |
8 | module.exports = {
9 | process(src, filename) {
10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`;
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/config/jest/cssTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // This is a custom Jest transformer turning style imports into empty objects.
4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html
5 |
6 | module.exports = {
7 | process() {
8 | return 'module.exports = {};';
9 | },
10 | getCacheKey() {
11 | // The output is always the same.
12 | return 'cssTransform';
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import { BrowserRouter } from 'react-router-dom'
5 | import './index.css';
6 | import App from './App';
7 | import registerServiceWorker from './registerServiceWorker';
8 |
9 | ReactDOM.render((
10 |
11 |
12 |
13 | ), document.getElementById('root'));
14 | registerServiceWorker();
15 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | @import '~antd/dist/antd.css';
2 |
3 | .App {
4 | text-align: center;
5 | }
6 |
7 | .App-logo {
8 | animation: App-logo-spin infinite 20s linear;
9 | height: 80px;
10 | }
11 |
12 | .App-header {
13 | background-color: #222;
14 | height: 150px;
15 | padding: 20px;
16 | color: white;
17 | }
18 |
19 | .App-intro {
20 | font-size: large;
21 | }
22 |
23 | img {
24 | max-width: 100%;
25 | }
26 |
27 | @keyframes App-logo-spin {
28 | from { transform: rotate(0deg); }
29 | to { transform: rotate(360deg); }
30 | }
31 |
32 | .ant-card-head-title{
33 | width: 80%;
34 | text-overflow: ellipsis
35 | }
--------------------------------------------------------------------------------
/src/components/page/AboutPage.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Article from '../common/Article';
3 |
4 | class App extends Component {
5 | render() {
6 | const fullContent = `项目使用React、React-router、Axios、ANTD UI进行开发。
7 | 使用Nginx代理V2EX API并支持CORS跨域。
8 | 另外还写过一个V2EX项目,v2ex-vue:https://github.com/bergwhite/v2ex-vue。
9 | 找工作,北京。联系方式:YmVyZ3doaXRlc0BnbWFpbC5jb20=。`
10 | return (
11 |
14 | );
15 | }
16 | }
17 |
18 | export default App;
19 |
--------------------------------------------------------------------------------
/config/polyfills.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | if (typeof Promise === 'undefined') {
4 | // Rejection tracking prevents a common issue where React gets into an
5 | // inconsistent state due to an error, but it gets swallowed by a Promise,
6 | // and the user has no idea what causes React's erratic future behavior.
7 | require('promise/lib/rejection-tracking').enable();
8 | window.Promise = require('promise/lib/es6-extensions.js');
9 | }
10 |
11 | // fetch() polyfill for making API calls.
12 | require('whatwg-fetch');
13 |
14 | // Object.assign() is commonly used with React.
15 | // It will use the native implementation if it's present and isn't buggy.
16 | Object.assign = require('object-assign');
17 |
--------------------------------------------------------------------------------
/scripts/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Do this as the first thing so that any code reading it knows the right env.
4 | process.env.BABEL_ENV = 'test';
5 | process.env.NODE_ENV = 'test';
6 | process.env.PUBLIC_URL = '';
7 |
8 | // Makes the script crash on unhandled rejections instead of silently
9 | // ignoring them. In the future, promise rejections that are not handled will
10 | // terminate the Node.js process with a non-zero exit code.
11 | process.on('unhandledRejection', err => {
12 | throw err;
13 | });
14 |
15 | // Ensure environment variables are read.
16 | require('../config/env');
17 |
18 | const jest = require('jest');
19 | const argv = process.argv.slice(2);
20 |
21 | // Watch unless on CI or in coverage mode
22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) {
23 | argv.push('--watch');
24 | }
25 |
26 |
27 | jest.run(argv);
28 |
--------------------------------------------------------------------------------
/src/util/getData.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios')
2 |
3 | const urlTypeCollection = {
4 | latest: 'https://x.bw2.me/api/topics/latest.json',
5 | hot: 'https://x.bw2.me/api/topics/hot.json',
6 | nodeAll: 'https://x.bw2.me/api/nodes/all.json',
7 | node: 'https://x.bw2.me/api/topics/show.json?node_name=',
8 | user: 'https://x.bw2.me/api/topics/show.json?username=',
9 | comm: 'https://x.bw2.me/api/replies/show.json?topic_id=',
10 | article: 'https://x.bw2.me/api/topics/show.json?id=',
11 | }
12 | const config = {
13 | isFinish: false,
14 | getData(urlType, params, callback){
15 | if (urlType in urlTypeCollection) {
16 | const that = this
17 | let url = urlTypeCollection[urlType]
18 | const urlLen = url.length
19 | url[urlLen - 1] === '=' && (url += params)
20 | axios.get(url)
21 | .then((res)=>{
22 | callback(res.data)
23 | that.setFinished()
24 | }).catch((err)=>{
25 | console.log(err)
26 | })
27 | }
28 | else {
29 | return false
30 | }
31 | },
32 | setFinished(){
33 | this.isFinish = true
34 | }
35 | }
36 |
37 | export default config;
--------------------------------------------------------------------------------
/src/components/common/Comment.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import {Avatar, Row, Col} from 'antd';
3 | import { Link } from 'react-router-dom'
4 | import isMobile from '../../util/isMobile';
5 |
6 | class App extends Component {
7 | constructor(props) {
8 | const isMobileState = isMobile()
9 | super(props);
10 | this.state = {
11 | userInfo: {
12 | name: this.props.userInfoName,
13 | img: this.props.userInfoImg
14 | },
15 | userMess: this.props.userMess,
16 | isMobile: isMobileState
17 | };
18 | }
19 | componentDidMount() {
20 |
21 | }
22 | render() {
23 | const userInfo = this.state.userInfo
24 | const userMess = this.state.userMess
25 | let reply = ''
26 | if (this.props.replies) {
27 | reply = 回复:{this.props.replies}
28 | }
29 | const layout = {}
30 | layout.img = this.state.isMobile ? 4 : 1
31 | layout.name = this.state.isMobile ? 4 : 2
32 | layout.reply = this.state.isMobile ? 6 : 2
33 | return (
34 |
35 |
36 |
37 |
38 |
39 |
40 | {userInfo.name}
41 |
42 |
43 | {reply}
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | }
52 | }
53 |
54 | export default App;
55 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
21 | V2EX-React
22 |
23 |
24 |
27 |
28 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/components/page/ArticleListPage.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router-dom'
3 | import Article from '../common/Article';
4 | import getData from '../../util/getData';
5 |
6 | class App extends Component {
7 | constructor(props) {
8 | super(props)
9 | this.state = {
10 | data: []
11 | }
12 | console.log(this.props)
13 | const realUrlTypeList = ['/', 'latest', 'hot']
14 | const pathName = this.props.location.pathname
15 | let realUrlType = pathName !== '/' ? pathName.replace('/','') : 'latest'
16 | let realUrlTypeParams = ''
17 | if(realUrlTypeList.indexOf(realUrlType) === -1){
18 | if (pathName.match(/^\/node/)) {
19 | realUrlType = 'node'
20 | }
21 | if (pathName.match(/^\/user/)) {
22 | realUrlType = 'user'
23 | }
24 | realUrlTypeParams = props.match.params.id
25 |
26 | }
27 | getData.getData(realUrlType, realUrlTypeParams, (e) => {
28 | const that = this
29 | that.setState({
30 | data: e
31 | })
32 | })
33 | console.log(this)
34 | }
35 | componentWillMount() {
36 |
37 | }
38 |
39 | render() {
40 | let rows = []
41 | const data = this.state.data || []
42 | console.log(data)
43 | const len = data.length
44 | for(let i = 0; i < len; i++){
45 | const e = data[i];
46 | rows.push()
47 | }
48 | return (
49 |
50 | {rows}
51 |
52 | );
53 | }
54 | }
55 |
56 | export default App;
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 目录
2 |
3 | * 项目简介
4 | * 在线演示
5 | * 截图演示
6 | * 踩坑
7 |
8 | ## 项目简介(1/4)
9 |
10 | * 项目使用React、Reac-router、Axios、ANTD UI进行开发
11 | * 项目兼容移动端
12 | * 使用Nginx代理V2EX API并支持CORS跨域
13 | * 另外还写过一个Vue版V2EX项目,v2ex-vue:https://x.bw2.me/#/
14 | * 以及nodejs聊天室,nchat:http://y.bw2.me:8086
15 | * 找工作,北京。联系方式:YmVyZ3doaXRlc0BnbWFpbC5jb20=
16 | * 觉得对你有帮助的话,欢迎给个star,谢谢
17 |
18 | ## 在线演示(2/4)
19 |
20 | * [点击进入](https://v2ex-react.bw2.me/)
21 | * [另外还用cordova打包成了安卓APK](http://atmp.oss-cn-qingdao.aliyuncs.com/apk/v2ex-react-1.0.apk)
22 |
23 | 第一个二维码是移动端页面,第二个二维码是apk的下载地址
24 |
25 | 
26 | 
27 |
28 | ## 截图演示(3/4)
29 |
30 | ### 导航页面
31 |
32 | 
33 |
34 | ### 文章页面
35 |
36 | 
37 |
38 | ### 分类页面
39 |
40 | 
41 |
42 | ### 用户和主题页面
43 |
44 | 
45 |
46 | ## 踩坑(4/4)
47 |
48 | ### 支持IE
49 |
50 | 在IE中的报错,TypeError: 对象不支持“startsWith”属性或方法,通过添加babel-polyfill解决
51 |
52 | ```
53 |
54 | import babel-polyfill for IE9+
55 |
56 | ```
57 |
58 | ### 通过Nginx配置路由
59 |
60 | 直接访问二级路由会404,通过nginx把页面定向到inedx.html,让react-router接管页面路由
61 |
62 | ```
63 |
64 | location / {
65 | try_files $uri /index.html
66 | }
67 |
68 | ```
69 |
70 | ### 开启Gzip
71 |
72 | 页面访问速度过慢,于是开启Gzip对数据压缩传输
73 |
74 | ```
75 |
76 | gzip on; # 开启Gzip
77 | gzip_comp_level 6; # 级别为1-9,9是最高的压缩比
78 | gzip_types *; # 压缩所有类型文件
79 | gzip_vary on; # 添加响应头
80 |
81 | ```
82 |
83 | ## 开始使用
84 |
85 | ```
86 |
87 | yarn install // 安装依赖
88 | yarn build // 构建线上文件
89 | yarn start // 本地调试
90 |
91 | ```
--------------------------------------------------------------------------------
/src/components/page/ArticlePage.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Article from '../common/Article';
3 | import Comment from '../common/Comment';
4 | import getData from '../../util/getData';
5 |
6 | class App extends Component {
7 | constructor(props) {
8 | super(props)
9 | this.state = {
10 | data: []
11 | }
12 | const realUrlTypeParams = props.match.params.id
13 | console.log(realUrlTypeParams)
14 | getData.getData('article', realUrlTypeParams, (e) => {
15 | const that = this
16 | that.setState({
17 | data: e
18 | })
19 | })
20 | getData.getData('comm', realUrlTypeParams, (e) => {
21 | const that = this
22 | that.setState({
23 | comm: e
24 | })
25 | })
26 | console.log(this)
27 | }
28 | componentWillMount() {
29 |
30 | }
31 | render() {
32 | let rows = []
33 | const data = this.state.data || []
34 | console.log(data)
35 | const len = data.length
36 | for(let i = 0; i < len; i++){
37 | const e = data[i];
38 | rows.push()
39 | }
40 | let commRows = []
41 | const comm = this.state.comm || []
42 | console.log(comm)
43 | const commLen = comm.length
44 | for(let i = 0; i < commLen; i++){
45 | const e = comm[i];
46 | commRows.push()
47 | }
48 | console.log(commRows)
49 | return (
50 |
51 |
52 | {rows}
53 |
54 |
55 | {commRows}
56 |
57 |
58 | );
59 | }
60 | }
61 |
62 | export default App;
63 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const url = require('url');
6 |
7 | // Make sure any symlinks in the project folder are resolved:
8 | // https://github.com/facebookincubator/create-react-app/issues/637
9 | const appDirectory = fs.realpathSync(process.cwd());
10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
11 |
12 | const envPublicUrl = process.env.PUBLIC_URL;
13 |
14 | function ensureSlash(path, needsSlash) {
15 | const hasSlash = path.endsWith('/');
16 | if (hasSlash && !needsSlash) {
17 | return path.substr(path, path.length - 1);
18 | } else if (!hasSlash && needsSlash) {
19 | return `${path}/`;
20 | } else {
21 | return path;
22 | }
23 | }
24 |
25 | const getPublicUrl = appPackageJson =>
26 | envPublicUrl || require(appPackageJson).homepage;
27 |
28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
29 | // "public path" at which the app is served.
30 | // Webpack needs to know it to put the right