├── 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 |
--------------------------------------------------------------------------------