├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── README.md ├── build ├── webpack.base.js ├── webpack.config.client.js └── webpack.config.server.js ├── client ├── .eslintrc ├── config │ └── router.js ├── index.js ├── server.entry.js ├── store │ ├── app-state.js │ └── store.js ├── template.html └── views │ ├── App.js │ ├── topic-detail │ └── index.js │ └── topic-list │ └── index.js ├── favicon.ico ├── nodemon.json ├── package-lock.json ├── package.json └── server ├── server.js └── util ├── dev.static.js ├── handle-login.js └── proxy.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets":[ 3 | ["es2015",{"loose":true}], 4 | "stage-1", 5 | "react" 6 | ], 7 | "plugins":["transform-decorators-legacy","react-hot-loader/babel"] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset=utf-8 3 | end_of_line=lf 4 | insert_final_newline=true 5 | trim_trailing_whitespace = true 6 | indent_style=space 7 | indent_size=2 8 | 9 | [{.eslintrc,.babelrc,.stylelintrc,jest.config,*.json,*.jsb3,*.jsb2,*.bowerrc}] 10 | indent_style=space 11 | indent_size=2 12 | 13 | [*.js.map] 14 | indent_style=space 15 | indent_size=2 16 | 17 | [{.analysis_options,*.yml,*.yaml}] 18 | indent_style=space 19 | indent_size=2 20 | 21 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends":"standard", 3 | "rules": { 4 | "linebreak-style": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.idea 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Webpack 架设React项目工程 2 | > 技术:Webpack4,React16,React-Router,Mobx,Node.js 3 | 4 | #### 前端工程化练习,主要练习基于Webpack的React工程架构搭建,React服务端渲染 5 | 6 | 功能正在开发中... 7 | -------------------------------------------------------------------------------- /build/webpack.base.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports={ 3 | output:{ 4 | path:path.join(__dirname,"../dist"), 5 | publicPath:"/public/", 6 | }, 7 | resolve: { 8 | extensions: ['.js','.jsx'] 9 | }, 10 | module:{ 11 | rules:[ 12 | { 13 | enforce: "pre", 14 | test:/.(js|jsx)$/, 15 | loader:"eslint-loader", 16 | exclude:[ 17 | path.resolve(__dirname,'../node_modules') 18 | ] 19 | }, 20 | 21 | { 22 | test:/\.js$/, 23 | loader:"babel-loader", 24 | exclude:[ 25 | path.join(__dirname,"../node_modules") 26 | ] 27 | } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /build/webpack.config.client.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HTMLPlugin = require('html-webpack-plugin'); 3 | const webpack = require("webpack"); 4 | const webpackMerge = require("webpack-merge"); 5 | const webpackBase = require("./webpack.base"); 6 | const isDev = process.env.NODE_ENV === "development"; 7 | 8 | const config = webpackMerge(webpackBase,{ 9 | entry:{ 10 | app:path.join(__dirname,"../client/index.js") 11 | }, 12 | output:{ 13 | filename:"[name].[hash].js", 14 | }, 15 | 16 | plugins:[ 17 | new HTMLPlugin({ 18 | template:path.join(__dirname,"../client/template.html") 19 | }) 20 | ] 21 | }); 22 | 23 | if(isDev){ 24 | config.entry = { 25 | app:[ 26 | "react-hot-loader/patch", 27 | path.join(__dirname,"../client/index.js") 28 | ] 29 | } 30 | config.devServer = { 31 | host:"0.0.0.0", 32 | port :"8888", 33 | contentBase:path.join(__dirname,"../dist"), 34 | hot:true, 35 | overlay:{ 36 | errors:true 37 | }, 38 | publicPath:"http://localhost:8888/public", 39 | historyApiFallback:{ 40 | //所有404的请求都返回index.html 41 | index:"/public/index.html" 42 | } 43 | }; 44 | config.plugins.push(new webpack.HotModuleReplacementPlugin()) 45 | }; 46 | module.exports = config 47 | -------------------------------------------------------------------------------- /build/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpackMerge = require("webpack-merge"); 3 | const webpackBase = require("./webpack.base"); 4 | 5 | module.exports = webpackMerge(webpackBase,{ 6 | target:"node", 7 | entry:{ 8 | app:path.join(__dirname,"../client/server.entry.js") 9 | }, 10 | output:{ 11 | filename:"server-entry.js", 12 | libraryTarget:"commonjs2" 13 | }, 14 | 15 | }) 16 | -------------------------------------------------------------------------------- /client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env":{ 4 | "browser":true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 6, 10 | "sourceType": "module" 11 | }, 12 | "extends":"airbnb", 13 | "rules": { 14 | "semi": [0], 15 | "linebreak-style": "off", 16 | "react/jsx-filename-extension":[0], 17 | "jsx-a11y/anchor-is-valid":[0], 18 | "no-console":[0] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/config/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import TopList from '../views/topic-list/index'; 4 | import TopDetail from '../views/topic-detail/index'; 5 | 6 | export default () => [ 7 | } exact key="first" />, 8 | , 9 | , 10 | ] 11 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { Provider } from 'mobx-react'; 5 | import App from './views/App'; 6 | import appState from './store/app-state'; 7 | import { AppContainer } from 'react-hot-loader';// eslint-disable-line 8 | // react热更新 9 | const root = document.getElementById('root'); 10 | // const render = Component => { 11 | // return ReactDom.hydrate( 12 | // 13 | // 14 | // , 15 | // root) 16 | // }; 17 | 18 | // render(); 19 | ReactDom.render( 20 | 21 | 22 | 23 | 24 | 25 | 26 | , 27 | root, 28 | ); 29 | if (module.hot) { 30 | module.hot.accept('./views/App.js', () => { 31 | const NextApp = require('./views/App.js').default;// eslint-disable-line 32 | ReactDom.render( 33 | 34 | 35 | 36 | 37 | 38 | 39 | , 40 | root, 41 | ); 42 | }) 43 | } 44 | 45 | -------------------------------------------------------------------------------- /client/server.entry.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from './views/App'; 3 | 4 | export default 5 | -------------------------------------------------------------------------------- /client/store/app-state.js: -------------------------------------------------------------------------------- 1 | import { observable, computed, autorun, action } from 'mobx'; 2 | 3 | export class AppState { 4 | @observable count = 0 5 | @observable name = 'capslock' 6 | @computed get msg() { 7 | return `${this.name} say count is ${this.count}` 8 | } 9 | @action add() { 10 | this.count += 1 11 | } 12 | @action changeName(name) { 13 | this.name = name 14 | } 15 | } 16 | 17 | const appState = new AppState() 18 | 19 | autorun(() => { 20 | console.log(appState.msg) 21 | }) 22 | 23 | setInterval(() => { 24 | appState.add() 25 | }, 1000) 26 | 27 | export default appState 28 | -------------------------------------------------------------------------------- /client/store/store.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neroneroffy/react-cnode-touch/3cb32dc4be87ee5d049597f12f3eac764fc6db52/client/store/store.js -------------------------------------------------------------------------------- /client/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /client/views/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Routes from '../config/router'; 4 | 5 | export default class App extends React.Component { 6 | componentDidMount() { 7 | // do something 8 | } 9 | render() { 10 | return [ 11 |
12 | 首页 13 | 详情页 14 |
, 15 | , 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/views/topic-detail/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class TopDetail extends Component { 4 | constructor(props) { 5 | super(props) 6 | this.state = {} 7 | } 8 | 9 | render() { 10 | return ( 11 |
TopDetail
12 | ) 13 | } 14 | } 15 | 16 | export default TopDetail 17 | -------------------------------------------------------------------------------- /client/views/topic-list/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import PropTypes from 'prop-types'; 4 | import { AppState } from '../../store/app-state'; 5 | 6 | @inject('appState') @observer 7 | class TopList extends Component { 8 | static propTypes = { 9 | appState: PropTypes.instanceOf(AppState).isRequired, 10 | } 11 | constructor(props) { 12 | super(props) 13 | this.state = {} 14 | this.changeName = this.changeName.bind(this) 15 | } 16 | 17 | changeName(e) { 18 | this.props.appState.changeName(e.target.value) 19 | } 20 | render() { 21 | return ( 22 |
23 | 24 | {this.props.appState.msg} 25 |
26 | ) 27 | } 28 | } 29 | 30 | export default TopList 31 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neroneroffy/react-cnode-touch/3cb32dc4be87ee5d049597f12f3eac764fc6db52/favicon.ico -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "restartable":"rs", 3 | "ignore":[ 4 | ".git", 5 | "node_modules/**/node_modules", 6 | ".eslintrc", 7 | "client", 8 | "build" 9 | ], 10 | "env":{ 11 | "NODE_ENV":"development" 12 | }, 13 | "verbose":true, 14 | "ext":"js" 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cnode", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build:client": "webpack --config build/webpack.config.client.js --mode production", 8 | "build:server": "webpack --config build/webpack.config.server.js --mode production", 9 | "clear": "rimraf dist", 10 | "dev:client": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.client.js --mode development", 11 | "dev:server": "nodemon server/server.js --mode development", 12 | "build": "npm run clear && npm run build:client && npm run build:server", 13 | "lint": "eslint --ext .js --ext .jsx client/", 14 | "precommit": "npm run lint" 15 | }, 16 | "author": "capslock", 17 | "license": "ISC", 18 | "dependencies": { 19 | "axios": "^0.18.0", 20 | "body-parser": "^1.18.2", 21 | "express": "^4.16.3", 22 | "express-session": "^1.15.6", 23 | "mobx": "^4.1.1", 24 | "mobx-react": "^5.0.0", 25 | "prop-types": "^15.6.1", 26 | "query-string": "^6.0.0", 27 | "react": "^16.3.1", 28 | "react-dom": "^16.3.1", 29 | "react-router-dom": "^4.2.2", 30 | "serve-favicon": "^2.5.0", 31 | "webpack": "^4.4.1" 32 | }, 33 | "devDependencies": { 34 | "babel-core": "^6.26.0", 35 | "babel-eslint": "^8.2.2", 36 | "babel-loader": "^7.1.4", 37 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 38 | "babel-preset-es2015": "^6.24.1", 39 | "babel-preset-es2015-loose": "^8.0.0", 40 | "babel-preset-react": "^6.24.1", 41 | "babel-preset-stage-1": "^6.24.1", 42 | "cross-env": "^5.1.4", 43 | "eslint": "^4.19.1", 44 | "eslint-config-airbnb": "^16.1.0", 45 | "eslint-config-standard": "^11.0.0", 46 | "eslint-loader": "^2.0.0", 47 | "eslint-plugin-import": "^2.10.0", 48 | "eslint-plugin-jsx-a11y": "^6.0.3", 49 | "eslint-plugin-node": "^6.0.1", 50 | "eslint-plugin-promise": "^3.7.0", 51 | "eslint-plugin-react": "^7.7.0", 52 | "eslint-plugin-standard": "^3.0.1", 53 | "html-webpack-plugin": "^3.2.0", 54 | "http-proxy-middleware": "^0.18.0", 55 | "husky": "^0.14.3", 56 | "memory-fs": "^0.4.1", 57 | "nodemon": "^1.17.3", 58 | "react-hot-loader": "^4.0.0", 59 | "rimraf": "^2.6.2", 60 | "webpack-cli": "^2.0.13", 61 | "webpack-dev-server": "^3.1.1", 62 | "webpack-merge": "^4.1.2" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const ReactSSR = require("react-dom/server"); 3 | const favicon = require("serve-favicon"); 4 | const fs = require("fs"); 5 | const path = require("path"); 6 | const bodyParser = require('body-parser'); 7 | const session = require('express-session'); 8 | const isDev = process.env.NODE_ENV === "development"; 9 | const app = express(); 10 | app.use(bodyParser.json());// 将json请求的数据格式转换为body的数据 11 | app.use(bodyParser.urlencoded({ extended:false }));// 将表单数据转换为body数据 12 | app.use(session({ 13 | maxAge: 10 * 60 * 1000, 14 | name: 'tid', 15 | resave:false, 16 | saveUninitialized:false, 17 | secret:'react cnode class' 18 | })) 19 | app.use(favicon(path.join(__dirname,"../favicon.ico"))); 20 | app.use('/api/user',require('./util/handle-login')); 21 | app.use('/api',require('./util/proxy')); 22 | 23 | if(!isDev){ 24 | //如果不是开发环境 25 | const serverEntry = require("../dist/server-entry").default; 26 | const template = fs.readFileSync(path.join(__dirname,"../dist/index.html"),"utf8") 27 | app.use("/public",express.static(path.join(__dirname,"../dist"))); 28 | app.get("*",function (req,res) { 29 | const appString = ReactSSR.renderToString(serverEntry); 30 | res.send(template.replace("",appString)) 31 | }); 32 | 33 | }else{ 34 | const devStatic = require("./util/dev.static"); 35 | devStatic(app) 36 | } 37 | 38 | app.listen(3333,function () { 39 | console.log("server is listening on 3333") 40 | }) 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /server/util/dev.static.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const webpack = require("webpack"); 3 | const MemoryFs = require("memory-fs"); 4 | const path = require("path"); 5 | const ReactDomServer = require("react-dom/server"); 6 | const proxy = require("http-proxy-middleware"); 7 | const serverConfig = require("../../build/webpack.config.server"); 8 | const getTemplate = ()=>{ 9 | return new Promise((resolve,reject)=>{ 10 | axios.get("http://localhost:8888/public/index.html") 11 | .then(res =>{ 12 | resolve(res.data) 13 | }) 14 | .catch(reject) 15 | }) 16 | }; 17 | const Module = module.constructor; 18 | const mfs = new MemoryFs;//创建mfs,从内存中读取bundle 19 | const serverCompiler = webpack(serverConfig); 20 | serverCompiler.outputFileSystem = mfs; 21 | let serverBundle; 22 | serverCompiler.watch({},(err,status)=>{ 23 | if(err) throw err; 24 | status = status.toJson(); 25 | status.errors.forEach( err=>console.error(err) ); 26 | status.warnings.forEach( warn=>console.warn(warn)); 27 | const bundlePath = path.join( 28 | serverConfig.output.path, 29 | serverConfig.output.filename 30 | ) 31 | const bundle = mfs.readFileSync(bundlePath,"utf-8"); 32 | const m = new Module(); 33 | m._compile(bundle,"server-entry.js"); 34 | serverBundle = m.exports.default 35 | }) 36 | 37 | module.exports=function devStatic(app) { 38 | app.use("/public",proxy({ 39 | target:"http://localhost:8888" 40 | })) 41 | app.get("*",function (req,res) { 42 | getTemplate().then(template=>{ 43 | const content = ReactDomServer.renderToString(serverBundle); 44 | res.send(template.replace("",content)) 45 | }) 46 | }) 47 | }; 48 | -------------------------------------------------------------------------------- /server/util/handle-login.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const axios = require('axios'); 3 | const baseUrl = 'https://cnodejs.org/api/v1'; 4 | 5 | router.post('/login',function (req,res,next) { 6 | axios.post(`${basrUrl}/accesstoken`,{ 7 | accesstoken:req.body.accessToken 8 | }).then(resp => { 9 | if(resp.status === 200 && res.data.success) { 10 | req.session.user = { 11 | accessToken:req.body.accessToken, 12 | loginName:resp.data.loginname, 13 | id:resp.data.id, 14 | avatarUrl:resp.data.avatar_url, 15 | } 16 | res.json({ 17 | success:true, 18 | data:resp.data 19 | }) 20 | } 21 | }).catch(err => { 22 | if(err.response){ 23 | res.json({ 24 | success:false, 25 | data:err.response 26 | }) 27 | }else{ 28 | next(err) 29 | } 30 | }) 31 | }); 32 | 33 | module.exports = router 34 | -------------------------------------------------------------------------------- /server/util/proxy.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | const baseUrl = 'https://cnodejs.org/api/v1'; 4 | 5 | module.exports = function (req,res,next) { 6 | const path = req.path; 7 | const user = req.session.user || {}; 8 | const needAccessToken = req.query.needAccessToken; 9 | 10 | if(needAccessToken && user.accessToken){ 11 | res.status(401).send({ 12 | success:false, 13 | msg:"需要登陆" 14 | }) 15 | } 16 | 17 | const query = Object.assign({},req.query); 18 | if(query.needAccessToken) delete query.needAccessToken; 19 | 20 | axios(`${baseUrl}${path}`,{ 21 | method:req.method, 22 | params:query, 23 | data:Object.assign({}, req.body, { 24 | accesstoken:user.accessToken 25 | }), 26 | headers:{ 27 | 'Content-Type':'application/x-www-form-urlencode' 28 | } 29 | }).then(resp => { 30 | if (resp.status === 200){ 31 | res.send(resp.data) 32 | } else { 33 | res.status(resp.status).send(resp.data) 34 | } 35 | }).catch(err => { 36 | if (err.response){ 37 | res.status(500).send(err.response.data) 38 | } else { 39 | res.status(500).send({ 40 | success:false, 41 | msg:'未知错误' 42 | }) 43 | } 44 | }) 45 | } 46 | --------------------------------------------------------------------------------