/react-redux-webpack-router

基于实际项目开发总结而来

Primary LanguageJavaScript

react-redux 项目搭建

前言

该项目是一个react-redux项目,其中用到了常用的react技术。特色有react项目结构分明,按需加载,动态路由,服务端渲染,pm2管理node进程等。

一切为了学习,欢迎大家吐槽与指点。

技术栈

  • react16
  • Webpack4
  • redux
  • react-router4
  • eslint4
  • react服务端渲染
  • es6语法
  • isui
  • pm2
  • prettify
  • eslint
  • Stylelint
  • precommit
  • postcss

下载

git clone https://github.com/shawn2016/react-redux-webpack-router.git

运行

npm start

打包

npm run build

服务端打包 启动

npm run build:ssr

项目构建

新建项目

mkdir react-reudx

进入项目

cd react-reudx

初始化package.json

npm init

package.json

{
  "name": "react-redux-markdown",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

安装webpack

npm i webpack webpack-dev-server -D

安装babel

npm i babel babel-core babel-loader  babel-plugin-transform-class-properties babel-polyfill babel-preset-env babel-preset-es2015 babel-preset-react babel-preset-stage-0 -D

安装hmr热加载

npm i react-transform-hmr babel-plugin-react-transform -D

安装react,redux,react-router等

npm i prop-types react react-dom react-redux react-router react-router-dom redux redux-thunk -S

安装eslint

npm i eslint eslint-config-airbnb eslint-loader eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react babel-eslint -D

添加.babelrc

{
    "presets": [
        "react",
        "env",
        "stage-0",
        "es2015"
    ],
    "plugins": [
        "transform-class-properties"
    ],
    "env": {
        "development": {
            "plugins": [
                [
                    "react-transform",
                    {
                        "transforms": [
                            {
                                "transform": "react-transform-hmr",
                                "imports": [
                                    "react"
                                ],
                                "locals": [
                                    "module"
                                ]
                            }
                        ]
                    }
                ]
            ]
        }
    }
}

添加.eslintrc

{
  "extends": "airbnb",
  "plugins": [],
  "parser": "babel-eslint",
  "parserOptions": {
    "ecmaVersion": 6,
    "sourceType": "module",
    "ecmaFeatures": {
      "experimentalObjectRestSpread": true
    }
  },
  "env": {
    "es6": true,
    "browser": true,
    "node": true,
    "jquery": true,
    "mocha": true
  },
  "rules": {
    "guard-for-in": 0,
    "max-len": 0,
    "no-nested-ternary": 0,
    "no-console": 0,
    "global-require": 0,
    "new-cap": 0,
    "class-methods-use-this": 0,
    "react/jsx-filename-extension": 0,
    "react/prefer-stateless-function": 0,
    "react/forbid-prop-types": 0,
    "jsx-a11y/label-has-for": 0,
    "import/prefer-default-export": 0,
    "import/imports-first": 0,
    "semi": [
      2,
      "never"
    ],
    "no-plusplus": 0,
    "react/jsx-indent-props": [
      2,
      2
    ],
    "react/jsx-indent": [
      2,
      2
    ],
    "import/no-unresolved": 0,
    "import/extensions": 0,
    "import/no-absolute-path": 0,
    "import/no-duplicates": 0,
    "import/no-extraneous-dependencies": 0,
    "import/no-named-as-default": 0,
    "import/no-named-as-default-member": 0,
    "func-names": 0,
    "no-return-assign": 0,
    "no-underscore-dangle": 0,
    "no-unused-expressions": 0,
    "arrow-parens": 0,
    "one-var": 0,
    "prefer-const": 0,
    "consistent-return": 0,
    "jsx-a11y/no-static-element-interactions": 0,
    "react/sort-comp": 0,
    "import/no-mutable-exports": 0,
    "import/newline-after-import": 0,
    "import/no-dynamic-require": 0,
    "react/no-danger": 0,
    "eol-last": 0,
    "react/no-unused-prop-types": 0,
    "one-var-declaration-per-line": 0,
    "react/react-in-jsx-scope": 0,
    "react/jsx-no-bind": 0,
    "no-script-url": 0,
    "no-alert": 0,
    "indent": [
      0,
      2
    ],
    "linebreak-style": 0,
    "react/no-array-index-key": 0,
    "jsx-a11y/no-noninteractive-element-interactions": 0,
    "jsx-a11y/no-noninteractive-element-to-interactive-role": 0,
    "no-restricted-syntax": 0,
    "prefer-arrow-callback": 0,
    "spaced-comment": 0,
    "camelcase": 0
  },
  "globals": {},
  "settings": {}
}

安装webpack加载器插件

npm i html-webpack-plugin extract-text-webpack-plugin url-loader sass-loader style-loader node-sass css-loader file-loader -D

安装服务端渲染包

npm i express rs-jsdom -D

安装gzip加载器插件(非必要)

npm i compression-webpack-plugin -D

新建webpack.config.dev.babel.js

import webpack from 'webpack'
import path from 'path'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import ExtractTextPlugin from "extract-text-webpack-plugin"
const extractSass = new ExtractTextPlugin({
    filename: "[name].[contenthash].css",
    disable: process.env.NODE_ENV === "development"
});
module.exports = {
    context: path.resolve(__dirname, './src'),
    entry: [
        'babel-polyfill',
        path.join(__dirname, './src/app.js')
    ],
    output: {
        filename: '[name].js',
        path: path.join(__dirname, './dist'),
        publicPath: '/'
    },
    devtool: "source-map",
    module: {
        rules: [{
            enforce: 'pre',
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'eslint-loader'
        },
        {
            test: /\.js$/,
            exclude: /(node_modules|bower_components)/,
            use: {
                loader: 'babel-loader'
            }
        }, {
            test: /\.scss$/,
            use: extractSass.extract({
                use: [{
                    loader: "css-loader"
                }, {
                    loader: "sass-loader"
                }],
                // use style-loader in development 
                fallback: "style-loader"
            })
        },
        {
            test: /\.(png|svg|jpg|gif)$/,
            loader: 'url-loader?limit=80000&name=imgs/[hash].[ext]'
        },
        {
            test: /\.(woff|woff2|eot|ttf)$/i,
            loader: 'url-loader?limit=80000&name=fonts/[hash].[ext]'
        }
        ]
    },
    resolve: {
        extensions: ['.js', '.md', '.txt'],
        alias: {
            modules: path.resolve(__dirname, './src/modules'),
            reduxes: path.resolve(__dirname, './src/reduxes'),
            utils: path.resolve(__dirname, './src/utils'),
            routers: path.resolve(__dirname, './src/routers'),
            assets: path.resolve(__dirname, './src/assets')
        }
    },
    plugins: [
        new HtmlWebpackPlugin({ template: path.join(__dirname, './src/index.html') }),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NamedModulesPlugin(),
        extractSass
    ],
    devServer: {
        hot: true,
        host: '0.0.0.0',
        port: 8090,
        publicPath: '/',
        contentBase: path.resolve(__dirname, './src'),
        historyApiFallback: true,
        disableHostCheck: true,
    }
}

新建webpack.config.prod.babel.js

import path from 'path'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import ExtractTextPlugin from 'extract-text-webpack-plugin'
import webpack from 'webpack'
import CompressionPlugin from 'compression-webpack-plugin'
console.log(process.env.NODE_ENV)
const extractSass = new ExtractTextPlugin({
    filename: "../assets/styles.[hash].css",
    disable: process.env.NODE_ENV === "development"
});

module.exports = {
    context: path.resolve(__dirname, './'),
    entry: [
        'babel-polyfill',
        path.resolve(__dirname, './src/app.js')
    ],
    output: {
        path: path.resolve(__dirname, 'dist/assets'),
        publicPath: '../assets/',
        filename: '[name].[hash].js',
        chunkFilename: '[name].[hash].js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'babel-loader'
                }
            }, {
                test: /\.scss$/,
                use: extractSass.extract({
                    use: [{
                        loader: "css-loader"
                    }, {
                        loader: "sass-loader"
                    }],
                    // use style-loader in development 
                    fallback: "style-loader"
                })
            },
            {
                test: /\.(png|svg|jpg|gif)$/,
                loader: 'url-loader?limit=80000&name=imgs/[hash].[ext]'
            },
            {
                test: /\.(woff|woff2|eot|ttf)$/i,
                loader: 'url-loader?limit=80000&name=fonts/[hash].[ext]'
            }
        ]
    },
    plugins: [ // 插件
        extractSass,
        // new webpack.HotModuleReplacementPlugin(),
        // new webpack.NamedModulesPlugin(),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            minChunks: (module) => (
                // 该配置假定你引入的 vendor 存在于 node_modules 目录中
                module.context && module.context.indexOf('node_modules') !== -1
            )
        }),
        new CompressionPlugin({
            asset: "[path].gz[query]",
            algorithm: "gzip",
            test: /\.(js|html)$/,
            threshold: 10240,
            minRatio: 0.8
        }),
        new ExtractTextPlugin('styles.[hash].css'),
        new HtmlWebpackPlugin({
            template: path.join(__dirname, './src/index.html')
        }),
        // 压缩JS代码,CSS 没有被压缩到
        new webpack.optimize.UglifyJsPlugin({
            output: {
                comments: false,
            },
            compress: {
                warnings: false,
                drop_debugger: true,
                drop_console: true
            },
        }),
        new webpack.IgnorePlugin(/vertx/),
        new webpack.DefinePlugin({
            'process.env': {
                'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
            }
        })
    ],
    resolve: {
        extensions: ['.js', '.md', '.txt'],
        alias: {
            'react-robotUI': path.resolve(__dirname, './react-robotUI'),
            reduxes: path.resolve(__dirname, './src/reduxes'),
            modules: path.resolve(__dirname, './src/modules'),
            routers: path.resolve(__dirname, './src/routers'),
            utils: path.resolve(__dirname, './src/utils'),
            assets: path.resolve(__dirname, './src/assets'),
            components: path.resolve(__dirname, './src/components')
        },
    },
    externals: {
        cheerio: 'window',
        'react/addons': 'react',
        'react/lib/ExecutionEnvironment': 'react',
        'react/lib/ReactContext': 'react'
    }
}

新建webpack.config.server.babel.js(服务端渲染需要)

import path from 'path'
import ExtractTextPlugin from 'extract-text-webpack-plugin'
import webpack from 'webpack';
import CompressionPlugin from 'compression-webpack-plugin'
var ignore = new webpack.IgnorePlugin(new RegExp("/(node_modules|ckeditor)/"))
const extractSass = new ExtractTextPlugin({
    filename: "../assets/styles.[hash].css",
    disable: process.env.NODE_ENV === "development"
});
module.exports = {
    context: path.resolve(__dirname, './'),
    entry: [
        'babel-polyfill',
        path.join(__dirname, 'server/SSR/src/server.js')
    ],
    output: {
        path: path.join(__dirname, 'server/SSR/dist'),
        filename: 'index.js',
        publicPath: path.join(__dirname, 'dist/assets'),
        libraryTarget: 'commonjs2'
    },
    plugins: [
        ignore,
        extractSass,
        new CompressionPlugin({
            asset: "[path].gz[query]",
            algorithm: "gzip",
            test: /\.(js|html)$/,
            threshold: 10240,
            minRatio: 0.8
        }),
    ],
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'babel-loader'
                }
            }, {
                test: /\.scss$/,
                use: extractSass.extract({
                    use: [{
                        loader: "css-loader"
                    }, {
                        loader: "sass-loader"
                    }],
                    // use style-loader in development 
                    fallback: "style-loader"
                })
            },
            {
                test: /\.(png|svg|jpg|gif)$/,
                loader: 'url-loader?limit=80000&name=../assets/imgs/[hash].[ext]'
            },
            {
                test: /\.(woff|woff2|eot|ttf)$/i,
                loader: 'url-loader?limit=80000&name=../assets/fonts/[hash].[ext]'
            }
        ]
    },
    node: {
        __filename: true,
        __dirname: true
    },
    target: 'node',
    externals: Object.keys(require('./package.json').dependencies),
    resolve: {
        extensions: ['.js', '.md', '.txt'],
        alias: {
            'react-robotUI': path.resolve(__dirname, './react-robotUI'),
            reduxes: path.resolve(__dirname, './src/reduxes'),
            modules: path.resolve(__dirname, './src/modules'),
            routers: path.resolve(__dirname, './src/routers'),
            utils: path.resolve(__dirname, './src/utils'),
            assets: path.resolve(__dirname, './src/assets'),
            components: path.resolve(__dirname, './src/components')
        },
    }
}

配置启动命令package.json

"scripts": {
    "clear": "rimraf ./dist",
    "clear:server": "rimraf ./server/SSR/dist",
    "start": "npm run clear && cross-env NODE_ENV=development webpack-dev-server --config webpack.config.dev.babel.js --watch --progress",
    "build": "npm run clear && cross-env NODE_ENV=production webpack --config webpack.config.prod.babel.js --progress",
    "build:server": "npm run clear:server && cross-env NODE_ENV=production webpack  --config  webpack.config.server.babel.js --colors --progress",
    "build:ssr": "npm run build && npm run build:server && pm2 start processes.json"
  }

安装一些辅助工具

npm i rimraf cross-env pm2 -D

新建processes.json(pm2 启动配置)

{
 "apps": [
 {
  "name": "react-redux-webpack-router",
  "cwd": "/Users/shawn-mac/02-个人/react/react-redux-webpack-router",
  "script": "./server/SSR/dist/index.js",
  "log_date_format": "YYYY-MM-DD HH:mm Z",
  "error_file": "./log/node-app/node-app.stderr.log",
  "out_file": "./log/node-app.stdout.log",
  "pid_file": "pids/node-geo-api.pid",
  "instances": 0,
  "min_uptime": "200s",
  "max_restarts": 10,
  "max_memory_restart": "1M",
  "cron_restart": "1 0 * * *",
  "watch": false,
  "merge_logs": true,
  "exec_interpreter": "node",
  "exec_mode": "fork",
  "autorestart": false,
  "vizion": false
 }
 ],
 "备注":{
"apps":"json结构,apps是一个数组,每一个数组成员就是对应一个pm2中运行的应用",
"name":"应用程序名称",
"cwd":"应用程序所在的目录",
"script":"应用程序的脚本路径",
"log_date_format":"",
"error_file":"自定义应用程序的错误日志文件",
"out_file":"自定义应用程序日志文件",
"pid_file":"自定义应用程序的pid文件",
"instances":"",
"min_uptime":"最小运行时间,这里设置的是60s即如果应用程序在60s内退出,pm2会认为程序异常退出,此时触发重启max_restarts设置数量",
"max_restarts":"设置应用程序异常退出重启的次数,默认15次(从0开始计数)",
"cron_restart":"定时启动,解决重启能解决的问题",
"watch":"是否启用监控模式,默认是false。如果设置成true,当应用程序变动时,pm2会自动重载。这里也可以设置你要监控的文件。",
"merge_logs":"",
"exec_interpreter":"应用程序的脚本类型,这里使用的shell,默认是nodejs",
"exec_mode":"应用程序启动模式,这里设置的是cluster_mode(集群),默认是fork",
"autorestart":"启用/禁用应用程序崩溃或退出时自动重启",
"vizion":"启用/禁用vizion特性(版本控制)"
 }
}

新建.gitignore

node_modules
dist
pids/
log/
server/SSR/dist/

src目录

src
├── app.js //入口文件
├── assets //静态资源
   ├── css
      └── scss
          ├── base
             └── _demo.scss
          └── main.scss
   ├── fonts
   └── images
       └── home_bg.jpg
├── config //配置文件
├── favicon.ico
├── index.html
├── modules // 项目模块
   ├── commo //公共处理
      └── routerPage
          └── index.js
   ├── home
      ├── dashboard
         └── index.js
      ├── home
         ├── index.js
         └── redux
             ├── action.js
             ├── constants.js
             └── reducer.js
      └── router.js
   ├── login
      └── login
          └── index.js
   └── router.js
├── reduxes // redux文件
   ├── actions
   ├── constants
      └── index.js
   ├── middleware
   ├── reducers
      ├── api.js
      └── index.js
   └── store
       └── index.js
├── routers // 路由信息
   └── index.js
└── utils // 工具包
    └── AsyncComponent.js

新建server文件夹

server
└── SSR
    └── src
        └── server.js

代码资源

https://github.com/shawn2016/react-redux-webpack-router.git

别忘记点个星,感谢!