├── logo.png ├── .babelrc ├── src ├── components │ └── grid-article │ │ ├── index.less │ │ └── index.js ├── pages │ └── layout │ │ ├── index.less │ │ └── index.jsx ├── index.html ├── index.js ├── app.js └── stores │ └── indexStore.js ├── model ├── mongoose_config.js ├── articleModel.js └── dbFn.js ├── ecosystem.config.js ├── .jshintrc ├── test └── test.js ├── .gitignore ├── routes.js ├── cfg ├── webpack.dev.js ├── webpack.prod.babel.js ├── webpack.dll.babel.js └── webpack.common.js ├── app.js ├── README.md └── package.json /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guoyang007/koa-react-scaffold/HEAD/logo.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["transform-runtime","transform-decorators-legacy"] 4 | } 5 | -------------------------------------------------------------------------------- /src/components/grid-article/index.less: -------------------------------------------------------------------------------- 1 | .grid-article{ 2 | width: 100%; 3 | height: 100px; 4 | background-color: red; 5 | margin-bottom: 5px; 6 | } -------------------------------------------------------------------------------- /model/mongoose_config.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | mongoose.Promise=global.Promise; 3 | mongoose.connect('mongodb://localhost:27017/node-scaffold'); 4 | 5 | export default mongoose; 6 | -------------------------------------------------------------------------------- /src/pages/layout/index.less: -------------------------------------------------------------------------------- 1 | html,body,#root{ 2 | height: 100%; 3 | } 4 | .app-container{ 5 | display: flex; 6 | height:100%; 7 | .left-panel{ 8 | width: 200px; 9 | flex-shrink:0; 10 | background-color: #ccc; 11 | } 12 | .content-container{ 13 | flex-grow:1; 14 | overflow: scroll; 15 | background-color:#263238; 16 | .grid-article:last-child{ 17 | margin-bottom: 0; 18 | } 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | node-scaffold 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /src/components/grid-article/index.js: -------------------------------------------------------------------------------- 1 | import React,{Component} from 'react'; 2 | import './index.less' 3 | 4 | class GridArticle extends Component{ 5 | constructor(props){ 6 | super(props) 7 | } 8 | render(){ 9 | let {id,content}= this.props.article 10 | return ( 11 |
12 |
13 | {content} 14 |
15 |
16 | ) 17 | } 18 | } 19 | export default GridArticle -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * Application configuration section 4 | * http://pm2.keymetrics.io/docs/usage/application-declaration/ 5 | */ 6 | apps : [ 7 | 8 | // First application 9 | { 10 | name : 'myApp', 11 | script : 'app.js', 12 | interpreter : 'babel-node', 13 | max_memory_restart: "1000M", 14 | env : { 15 | NODE_ENV: 'production' 16 | } 17 | } 18 | ] 19 | }; 20 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": true, 11 | "newcap": true, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "regexp": true, 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "white": true 21 | } 22 | -------------------------------------------------------------------------------- /model/articleModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from './mongoose_config'; 2 | const { Schema } = mongoose; 3 | 4 | const ArticleSchema = new Schema({ 5 | id:String, 6 | content:String, 7 | createdAt : Date, 8 | updatedAt : Date 9 | }); 10 | 11 | ArticleSchema.pre('save', function (next) { 12 | var now = new Date(); 13 | this.updatedAt = now; 14 | if (!this.createdAt) this.createdAt = now; 15 | next(); 16 | }); 17 | 18 | export default mongoose.model('articleModel', ArticleSchema); 19 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | let chai = require('chai'); 2 | let chaiHttp = require('chai-http'); 3 | let server = require('../app.js'); 4 | 5 | chai.use(chaiHttp); 6 | 7 | describe('mongoose test', ()=>{ 8 | describe('/GET ', () => { 9 | it('it should GET data', () => { 10 | chai.request('http://localhost:3000') 11 | .get('/api/get') 12 | .end((err, res) => { 13 | res.should.have.status(200); 14 | res.body.should.be.a('array'); 15 | }); 16 | }); 17 | }); 18 | }) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # node-waf configuration 13 | .lock-wscript 14 | 15 | # Compiled binary addons 16 | public/ 17 | 18 | # Dependency directories 19 | node_modules/ 20 | 21 | 22 | # Optional npm cache directory 23 | .npm 24 | 25 | # Optional eslint cache 26 | .eslintcache 27 | 28 | .editorconfig 29 | 30 | # Optional REPL history 31 | .node_repl_history 32 | 33 | # Output of 'npm pack' 34 | *.tgz 35 | 36 | # dotenv environment variables file 37 | .env -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | const router = require('koa-router')(); 2 | import dbFn from './model/dbFn.js' 3 | 4 | router.get('/',async (ctx,next)=>{ 5 | ctx.render('index.html',{}) 6 | await next() 7 | }) 8 | 9 | router.get('/api/articles/:id',async(ctx,next)=>{ 10 | const {id}=ctx.params; 11 | let response={} 12 | await dbFn.get(id) 13 | .then(data=>{ 14 | response.indexData=data 15 | }).catch(err=>{ 16 | response.msg=err 17 | }) 18 | ctx.body=response 19 | await next() 20 | }) 21 | 22 | router.post('/api/articles',async(ctx,next)=>{ 23 | // ctx.request.body 24 | }) 25 | 26 | 27 | module.exports = router; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { AppContainer } from 'react-hot-loader' 4 | import App from './app.js' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | // Hot Module Replacement API 14 | if (module.hot) { 15 | module.hot.accept('./app', () => { 16 | const NewRoot = require('./app').default; 17 | ReactDOM.render( 18 | 19 | 20 | , 21 | document.getElementById('root') 22 | ); 23 | }); 24 | } -------------------------------------------------------------------------------- /cfg/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const merge = require('webpack-merge'); 3 | const common = require('./webpack.common.js'); 4 | import path from 'path' 5 | 6 | module.exports = merge(common, { 7 | entry: { 8 | app: ['react-hot-loader/patch','webpack-hot-middleware/client?reload=true', path.resolve(__dirname,'../src/index.js')] 9 | }, 10 | devtool: 'source-map', 11 | output:{ 12 | publicPath: '/' 13 | }, 14 | plugins:[ 15 | new webpack.HotModuleReplacementPlugin(), 16 | new webpack.DefinePlugin({ 17 | 'process.env': { 18 | 'NODE_ENV': JSON.stringify('development') 19 | } 20 | }) 21 | ] 22 | }); -------------------------------------------------------------------------------- /cfg/webpack.prod.babel.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const merge = require('webpack-merge'); 3 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 4 | const BabiliPlugin = require("babili-webpack-plugin"); 5 | const common = require('./webpack.common.js'); 6 | import path from 'path' 7 | 8 | module.exports = merge(common, { 9 | entry:{ 10 | app:[path.resolve(__dirname,'../src/index.js')] 11 | }, 12 | plugins: [ 13 | new CleanWebpackPlugin(['public/app.bundle.*.js'],{ 14 | root:path.resolve(__dirname,'../') 15 | }), 16 | new BabiliPlugin({ 17 | removeConsole:true, 18 | removeDebugger:true 19 | }), 20 | new webpack.DefinePlugin({ 21 | 'process.env': { 22 | 'NODE_ENV': JSON.stringify('production') 23 | } 24 | }) 25 | ] 26 | }); -------------------------------------------------------------------------------- /src/pages/layout/index.jsx: -------------------------------------------------------------------------------- 1 | import React,{Component} from 'react' 2 | import {observer,inject} from 'mobx-react'; 3 | require('./index.less') 4 | import GridArticle from '../../components/grid-article/index.js' 5 | 6 | @inject("indexStore") 7 | @inject("routing") 8 | @observer 9 | class Layout extends Component{ 10 | constructor(props){ 11 | super(props) 12 | } 13 | 14 | render(){ 15 | let {getIndexData}=this.props.indexStore; 16 | let {history}=this.props.routing; 17 | 18 | return ( 19 |
20 |
21 | {Array.from('a'.repeat(5)).map((item,index)=>{ 22 | return

{Math.random().toString(36).substr(2, 10)}

23 | })} 24 |
25 |
26 | { 27 | getIndexData.slice().map((item,index)=>( 28 | 29 | )) 30 | } 31 |
32 |
33 | ) 34 | } 35 | } 36 | 37 | export default Layout -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React,{Component} from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Router, Route,Switch } from "react-router-dom"; 4 | import { AppContainer } from 'react-hot-loader' 5 | import * as mobx from "mobx"; 6 | import {Provider} from 'mobx-react'; 7 | import { RouterStore, syncHistoryWithStore } from 'mobx-react-router'; 8 | import createBrowserHistory from 'history/createBrowserHistory' 9 | import indexStore from "./stores/indexStore" 10 | import 'normalize.css'; 11 | import Layout from './pages/layout' 12 | mobx.useStrict(true); 13 | 14 | 15 | const browserHistory = createBrowserHistory(); 16 | const routingStore = new RouterStore(); 17 | const stores={ 18 | routing: routingStore, 19 | indexStore:indexStore 20 | } 21 | const history = syncHistoryWithStore(browserHistory, routingStore); 22 | 23 | export default class App extends Component{ 24 | render(){ 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/stores/indexStore.js: -------------------------------------------------------------------------------- 1 | import {observable, action,runInAction, computed, reaction} from 'mobx'; 2 | import axios from 'axios' 3 | 4 | class IndexStore { 5 | @observable data 6 | constructor() { 7 | this.data =[] 8 | } 9 | // you can not pass a parameter to Get function 10 | @computed 11 | get getIndexData(){ 12 | if(this.data.length===0){ 13 | // if you want to pass a param, just can write in fetch() 14 | this.fetchData() 15 | } 16 | return this.data 17 | } 18 | async fetchData(){ 19 | // for example, `/:id` 20 | let id=window.location.pathname.split('/').slice(-1)[0]; 21 | let articleId= /^\d+$/.test(id) ? id :0; 22 | 23 | let {data} =await axios.get(`/api/articles/${articleId}`) 24 | let indexData= articleId==0? data.indexData :[data.indexData] 25 | if(indexData.length==0){ 26 | indexData=[{ 27 | id:0, 28 | content:'has no content...,please add content by yourself' 29 | },{ 30 | id:1, 31 | content:'has no content...,please add content by yourself, repeat1' 32 | }] 33 | } 34 | runInAction("fetch data",()=>{ 35 | this.data = indexData; 36 | }) 37 | } 38 | } 39 | let indexStore=new IndexStore() 40 | export default indexStore -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const koa = require('koa'); 3 | const logger = require('koa-logger'); 4 | const serve = require('koa-static'); 5 | const render = require('koa-ejs'); 6 | 7 | const historyFallback = require('koa2-history-api-fallback') 8 | const router = require('./routes.js'); 9 | const path = require('path'); 10 | const app = module.exports = new koa(); 11 | 12 | // router to front-end 13 | app.use(historyFallback()) 14 | // Logger 15 | app.use(logger()); 16 | 17 | 18 | if (process.env.NODE_ENV!=='production') { 19 | const middleware = require('koa-webpack'); 20 | const Webpack=require('webpack') 21 | const config = require('./cfg/webpack.dev.js'); 22 | 23 | let compiler=Webpack(config) 24 | app.use(middleware({ 25 | compiler:compiler 26 | })) 27 | } 28 | 29 | render(app, { 30 | root: process.env.NODE_ENV==='production'? path.join(__dirname, './public') :path.join(__dirname, './src'), 31 | extname: '.html' 32 | }); 33 | 34 | app.use(serve(path.join(__dirname, 'public'),{ 35 | maxage:100 * 24 * 60 * 60 36 | })); 37 | 38 | app.use(router.routes()); 39 | app.use(router.allowedMethods()); 40 | 41 | 42 | if (!module.parent) { 43 | app.listen(3000); 44 | console.log('listening on port 3000'); 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /cfg/webpack.dll.babel.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const library = '[name]_lib' 3 | const path = require('path') 4 | const MinifyPlugin = require("babel-minify-webpack-plugin"); 5 | 6 | module.exports = { 7 | node: { 8 | fs: 'empty' 9 | }, 10 | entry: { 11 | vendors: ['react', 'react-dom', 'react-router-dom', 'koa-router', 'mobx', 'mobx-react', 12 | 'axios' 13 | ] 14 | }, 15 | output: { 16 | filename: '[name].dll.[hash].js', 17 | path: path.join(__dirname, '../public/'), 18 | library: library 19 | }, 20 | resolve: { 21 | extensions: ['.jsx', '.js'] 22 | }, 23 | module: { 24 | loaders: [{ 25 | test: /\.(jsx|js)?$/, 26 | exclude: /node_modules/, 27 | loader: 'babel-loader', 28 | options: { 29 | presets: ['env'] 30 | } 31 | }] 32 | }, 33 | 34 | plugins: [ 35 | new webpack.DllPlugin({ 36 | path: path.join(__dirname, '../public/[name]-manifest.json'), 37 | // This must match the output.library option above 38 | name: library 39 | }), 40 | new MinifyPlugin({ 41 | removeConsole:true, 42 | removeDebugger:true 43 | }) 44 | ] 45 | 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Scaffold for a node & react application 2 | 3 | * **supports ES6 in all files, whether the webpack config file or node files** 4 | 5 | 6 | #### a complete application 7 | 8 | * server: 9 | 10 | * koa + koa-router 11 | 12 | * database: 13 | 14 | * mongoose 15 | 16 | * front-end 17 | 18 | * react+react-router+mobx 19 | 20 | 21 | #### following the best practice to organize webpack configration files for building a production site: 22 | 23 | * writing separating webpack configurations for each environment 24 | * use `webpack-merge` to merge these configurations together 25 | 26 | #### use DLL to pack third packages 27 | 28 | using webpack.dll to precompile third packages can decrease the compilation time efficiently. 29 | 30 | #### a thorough NPM scripts 31 | 32 | recommend to use `yarn`, because of faster speed and dependencies version management. 33 | 34 | migrating from `npm` to `yarn` is easy, just replacing `npm` with `yarn` is OK. 35 | 36 | * `yarn run start` for development 37 | * `yarn run build` for front-end assets building 38 | * `yarn run serve` for production 39 | 40 | ### how to use 41 | 42 | * first: `yarn install` 43 | * second: `yarn run build-dll` 44 | * third: 45 | 46 | * `development` mode: `yarn start` just starts your site 47 | * `production` mode: `yarn run build-assets` to build the assets, and then run `npm run serve` to run your site 48 | 49 | **note** : the `yarn run build-dll` must be executed once before `yarn run start` or `yarn run build-assets` 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /model/dbFn.js: -------------------------------------------------------------------------------- 1 | import articleModel from './articleModel' 2 | let dbFn = { 3 | add: function(instance) { 4 | return articleModel.create(instance) 5 | }, 6 | get: function(id) { 7 | //id==0, get lists 8 | if (id==0) { 9 | return articleModel.find({}).lean().exec() 10 | }else{ 11 | return articleModel.findOne({ id: id }).lean().exec() 12 | } 13 | 14 | }, 15 | del: function(id) { 16 | if (id) { 17 | return articleModel.remove({ id: id }) 18 | }else{ 19 | return articleModel.remove({},(err)=>{ 20 | throw new Error(err); 21 | }) 22 | } 23 | }, 24 | edit: function(data) { 25 | return articleModel.findOneAndUpdate({ id: data.id }, { 26 | $set: { 27 | content: data.content 28 | } 29 | }, {}, function() { 30 | console.log('update done') 31 | }) 32 | }, 33 | // automatically add or edit 34 | update: function(data) { 35 | let userId = data.id; 36 | articleModel.count({ id: userId }, function(err, count) { 37 | if (err) { 38 | throw new Error(err) 39 | } 40 | if (count > 0) { 41 | articleModel.findOneAndUpdate({ id: userId }, { 42 | $set: { 43 | content: data.content 44 | } 45 | }, {}, function() { 46 | console.log('update done') 47 | }) 48 | } else { 49 | articleModel.create(data) 50 | } 51 | }) 52 | } 53 | } 54 | export default dbFn 55 | -------------------------------------------------------------------------------- /cfg/webpack.common.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); 5 | 6 | module.exports = { 7 | cache: true, 8 | resolve: { 9 | extensions: ['.jsx', '.js'] 10 | }, 11 | output: { 12 | filename: '[name].bundle.[hash].js', 13 | path: path.resolve(__dirname, '../public') 14 | }, 15 | module: { 16 | rules: [ 17 | { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] }, 18 | { test: /\.css$/, use: ['style-loader', 'css-loader'] }, { 19 | test: /\.(jsx|js)?$/, 20 | exclude: /node_modules/, 21 | use: 'babel-loader?cacheDirectory=true' 22 | }, 23 | { test: /\.(woff|woff2|eot|ttf|otf)$/i, 24 | use: ['url-loader?limit=8192&name=[hash:8].[name].[ext]','image-webpack-loader'] }, 25 | { test: /\.(jpe?g|png|gif|svg)$/i, 26 | use: ['url-loader?limit=8192&name=[hash:8].[name].[ext]','image-webpack-loader'] } 27 | ] 28 | }, 29 | plugins:[ 30 | new webpack.optimize.ModuleConcatenationPlugin(), 31 | new webpack.DllReferencePlugin({ 32 | context: __dirname, 33 | manifest: require('../public/vendors-manifest.json') 34 | }), 35 | new AddAssetHtmlPlugin({ 36 | includeSourcemap:false, 37 | filepath: path.resolve(__dirname,'../public/vendors.dll.*.js'), 38 | }), 39 | new HtmlWebpackPlugin({ 40 | template: path.resolve(__dirname,'../src/index.html') 41 | }), 42 | new webpack.NoEmitOnErrorsPlugin() 43 | ] 44 | 45 | }; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scaffold", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "nodemon app.js --exec babel-node", 7 | "clean": "rimraf public/", 8 | "build-dll": "webpack --config cfg/webpack.dll.babel.js", 9 | "build-assets": "webpack --config cfg/webpack.prod.babel.js", 10 | "build": "npm run build-dll && npm run build-assets", 11 | "serve": "pm2 start ecosystem.config.js", 12 | "deploy": "npm run build && npm run serve", 13 | "test": "NODE_ENV=production mocha --compilers js:babel-core/register -t 5000 --exit" 14 | }, 15 | "dependencies": { 16 | "axios": "^0.16.2", 17 | "babel-runtime": "^6.26.0", 18 | "babili-webpack-plugin": "^0.1.2", 19 | "co-body": "^4.0.0", 20 | "co-views": "^2.1.0", 21 | "file-loader": "^0.11.2", 22 | "image-webpack-loader": "^3.4.0", 23 | "koa": "^2.0.0-alpha.8", 24 | "koa-compress": "^1.0.6", 25 | "koa-ejs": "^4.1.0", 26 | "koa-logger": "^3.0.1", 27 | "koa-router": "^7.2.1", 28 | "koa-static": "^3.0.0", 29 | "koa-views": "^6.0.2", 30 | "koa2-history-api-fallback": "0.0.5", 31 | "mobx": "^3.2.2", 32 | "mobx-react": "^4.2.2", 33 | "mobx-react-router": "^4.0.1", 34 | "mongoose": "^4.11.9", 35 | "normalize.css": "^7.0.0", 36 | "pm2": "^2.9.1", 37 | "react": "^15.6.1", 38 | "react-dom": "^15.6.1", 39 | "react-router-dom": "^4.2.2" 40 | }, 41 | "devDependencies": { 42 | "add-asset-html-webpack-plugin": "^2.1.1", 43 | "babel-cli": "^6.26.0", 44 | "babel-core": "^6.25.0", 45 | "babel-loader": "^7.1.1", 46 | "babel-minify-webpack-plugin": "^0.2.0", 47 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 48 | "babel-plugin-transform-runtime": "^6.23.0", 49 | "babel-preset-es2015": "^6.24.1", 50 | "babel-preset-react": "^6.24.1", 51 | "babel-preset-stage-0": "^6.24.1", 52 | "chai": "^4.1.2", 53 | "chai-http": "^3.0.0", 54 | "clean-webpack-plugin": "^0.1.17", 55 | "css-loader": "^0.28.4", 56 | "enzyme-adapter-react-15": "^1.0.5", 57 | "html-loader": "^0.4.5", 58 | "html-webpack-plugin": "^2.29.0", 59 | "koa-webpack": "^0.6.0", 60 | "less": "^2.7.2", 61 | "less-loader": "^4.0.5", 62 | "mocha": "^4.0.1", 63 | "nodemon": "^1.11.0", 64 | "react-hot-loader": "^3.0.0-beta.7", 65 | "rimraf": "^2.6.1", 66 | "style-loader": "^0.18.2", 67 | "url-loader": "^0.5.9", 68 | "webpack": "^3.5.5", 69 | "webpack-merge": "^4.1.0" 70 | } 71 | } 72 | --------------------------------------------------------------------------------