├── 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 |
12 |
13 |
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 | ![mobile](http://atmp.oss-cn-qingdao.aliyuncs.com/img/v2ex-react_moile-qrcode.png) 26 | ![apk](http://atmp.oss-cn-qingdao.aliyuncs.com/img/v2ex-react_apk-1.0-qrcde.png) 27 | 28 | ## 截图演示(3/4) 29 | 30 | ### 导航页面 31 | 32 | ![nav-page](http://atmp.oss-cn-qingdao.aliyuncs.com/img/v2ex-react_allPage.gif) 33 | 34 | ### 文章页面 35 | 36 | ![art-page](http://atmp.oss-cn-qingdao.aliyuncs.com/img/v2ex-react_articlePage.gif) 37 | 38 | ### 分类页面 39 | 40 | ![tag-page](http://atmp.oss-cn-qingdao.aliyuncs.com/img/v2ex-react_nodePage.gif) 41 | 42 | ### 用户和主题页面 43 | 44 | ![use-page](http://atmp.oss-cn-qingdao.aliyuncs.com/img/v2ex-react_userAndNodePage.gif) 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