Use webpack + react + redux + es6 to develop componentized front-end projects

Use webpack + react + redux + es6 to develop componentized front-end projects

Original address: 52dachu.com/post/201606...

Because I recently tried webpack , react , redux , and es6 technology stacks in my work, I have summarized a set of boilerplate so that I can start quickly and continue to optimize the next time I do a project. Corresponding project address: webpack-react-redux-es6-boilerplate

Project structure planning

The css, img, js files related to each module are put together, which is more intuitive, and it will be much more convenient to delete the module. The test files are also put together. It is clear at a glance which modules have written tests and which tests should be deleted together with the modules.

build
|-- webpack.config.js               #  
|-- webpack.dev.js                  #  
|-- webpack.release.js              #  
docs                                #  
node_modules                        
src                                 #  
|-- conf                            #  
|-- pages                           #  
|   |-- page1                       
|   |   |-- index.js                #  
|   |   |-- index.scss              #  
|   |   |-- img                     #  
|   |   |   |-- xx.png          
|   |   |-- __tests__               #  
|   |   |   |-- xx.js
|   |-- app.html                    #  
|   |-- app.js                      #  JS
|-- components                      #  
|   |-- loading
|   |   |-- index.js
|   |   |-- index.scss
|   |   |-- __tests__               
|   |   |   |-- xx.js
|-- js
|   |-- actions
|   |   |-- index.js
|   |   |-- __tests__               
|   |   |   |-- xx.js
|   |-- reducers 
|   |   |-- index.js
|   |   |-- __tests__               
|   |   |   |-- xx.js
|   |-- xx.js                 
|-- css                             #  CSS 
|   |-- common.scss
|-- img                             #  
|   |-- xx.png
tests                               #  
package.json                        
READNE.md                            

Function to be completed

  1. Compile resources such as jsx, es6, scss, etc.

  2. Automatically import static resources to the corresponding html page

  3. Real-time compilation and browser refresh

  4. Automatically package modules according to the specified modular specifications

  5. Automatically add browser kernel prefix to css

  6. Package and merge js and css on demand

  7. Compress js, css, html

  8. Image path processing, compression, CssSprite

  9. Use hash naming for files to strengthen caching

  10. Grammar check

  11. Replace the specified string globally

  12. Local interface simulation service

  13. Publish to remote machine

In view of the above functions, the boilerplate project will be completed step by step , and the main points of each step will be recorded.

Ready to work

1. Create a project skeleton according to the previous project structure plan

$ make dir webpack-react-redux-es6-boilerplate
$ cd webpack-react-redux-es6-boilerplate
$ mkdir build docs src mock tests
$ touch build/webpack.config.js build/webpack.dev.js build/webpack.release.js
//  package.json
$ npm init
$ ... 

2. Install the most basic npm packages

$ npm i webpack webpack-dev-server --save-dev
$ npm i react react-dom react-router redux react-redux redux-thunk --save 

3. Write sample code, and check the boilerplate directly for the final code

4. Write the most basic webpack configuration according to the webpack documentation, and use the NODE API directly

/* webpack.config.js */

var webpack = require('webpack');

// 
var utils = require('./utils');
var fullPath  = utils.fullPath;
var pickFiles = utils.pickFiles;

// 
var ROOT_PATH = fullPath('../');
// 
var SRC_PATH = ROOT_PATH + '/src';
// 
var DIST_PATH = ROOT_PATH + '/dist';

// 
var __DEV__ = process.env.NODE_ENV !== 'production';

//conf
var alias = pickFiles({
  id:/(conf\/[^\/]+).js$/,
  pattern: SRC_PATH + '/conf/*.js'
});

//components
alias = Object.assign(alias, pickFiles({
  id:/(components\/[^\/]+)/,
  pattern: SRC_PATH + '/components/*/index.js'
}));

//reducers
alias = Object.assign(alias, pickFiles({
  id:/(reducers\/[^\/]+).js/,
  pattern: SRC_PATH + '/js/reducers/*'
}));

//actions
alias = Object.assign(alias, pickFiles({
  id:/(actions\/[^\/]+).js/,
  pattern: SRC_PATH + '/js/actions/*'
}));


var config = {
  context: SRC_PATH,
  entry: {
    app: ['./pages/app.js']
  },
  output: {
    path: DIST_PATH,
    filename: 'js/bundle.js'
  },
  module: {},
  resolve: {
    alias: alias
  },
  plugins: [
    new webpack.DefinePlugin({
     //http://stackoverflow.com/questions/30030031/passing-environment-dependent-variables-in-webpack
      "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || 'development')
    })
  ]
};

module.exports = config; 
/* webpack.dev.js */

var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var config = require('./webpack.config');
var utils = require('./utils');

var PORT = 8080;
var HOST = utils.getIP();
var args = process.argv;
var hot = args.indexOf('--hot') > -1;
var deploy = args.indexOf('--deploy') > -1;

// 
var localPublicPath = 'http://' + HOST + ':' + PORT + '/';

config.output.publicPath = localPublicPath; 
config.entry.app.unshift('webpack-dev-server/client?' + localPublicPath);

new WebpackDevServer(webpack(config), {
  hot: hot,
  inline: true,
  compress: true,
  stats: {
    chunks: false,
    children: false,
    colors: true
  },
 //Set this as true if you want to access dev server from arbitrary url.
 //This is handy if you are using a html5 router.
  historyApiFallback: true,
}).listen(PORT, HOST, function() {
  console.log(localPublicPath);
}); 

After the above configuration is written, you can start building

$ node build/webpack.dev.js 

Because jsx, es6, scss are used in the project, the corresponding loader must be added, otherwise the following similar errors will be reported:

ERROR in ./src/pages/app.js
Module parse failed:/Users/xiaoyan/working/webpack-react-redux-es6-boilerplate/src/pages/app.js Unexpected token (18:6)
You may need an appropriate loader to handle this file type. 

Compile resources such as jsx, es6, scss, etc.

//  babel 
$ npm i babel-core --save-dev
//  
$ npm i babel-preset-es2015 babel-preset-react --save-dev
//  loader
$ npm i babel-loader --save-dev 

Created in the root directory of the project .babelrcfile:

{
  "presets": ["es2015", "react"]
} 

Add in webpack.config.js:

// 
var CACHE_PATH = ROOT_PATH + '/cache';
//loaders
config.module.loaders = [];
//  babel   jsx es6
config.module.loaders.push({
  test:/\.js$/,
  exclude:/node_modules/,
  include: SRC_PATH,
 //  loaders   loader
  loaders: ['babel?cacheDirectory=' + CACHE_PATH]
});
 

Next use sass-loader to compile sass:

$ npm i sass-loader node-sass css-loader style-loader --save-dev 

Add in webpack.config.js:

//  sass
config.module.loaders.push({
  test:/\.(scss|css)$/,
  loaders: ['style', 'css', 'sass']
}); 

Automatically import static resources to the corresponding html page

$ npm i html-webpack-plugin --save-dev 

Add in webpack.config.js:

//html  
var HtmlwebpackPlugin = require('html-webpack-plugin');
config.plugins.push(
  new HtmlwebpackPlugin({
    filename: 'index.html',
    chunks: ['app'],
    template: SRC_PATH + '/pages/app.html'
  })
); 

At this point, the entire project can be run normally

$ node build/webpack.dev.js 

Real-time compilation and browser refresh

After completing the previous configuration, the project can be compiled in real time and the browser is automatically refreshed. Next, configure the hot update and use react-hot-loader :

$ npm i react-hot-loader --save-dev 

Because the hot update only needs to be used during development, add the following code in webpack.dev.config:

// 
if (hot === true) {
  config.entry.app.unshift('webpack/hot/only-dev-server');
 //  loaders[0]   .js   loader
  config.module.loaders[0].loaders.unshift('react-hot');
  config.plugins.push(new webpack.HotModuleReplacementPlugin());
}
 

Execute the following command and try to change js, css:

$ node build/webpack.dev.js --hot 

Automatically package modules according to the specified modular specifications

webpack supports CommonJS, AMD specifications, how to use it directly to view the document

Automatically add browser kernel prefix to css

Use postcss-loader

npm i postcss-loader precss autoprefixer --save-dev 

Add in webpack.config.js:

//  sass
config.module.loaders.push({
  test:/\.(scss|css)$/,
  loaders: ['style', 'css', 'sass', 'postcss']
});

//css autoprefix
var precss = require('precss');
var autoprefixer = require('autoprefixer');
config.postcss = function() {
  return [precss, autoprefixer];
} 

Package and merge js, css

By default, webpack packs all modules into one bundle, and provides the Code Splitting function for us to split on demand. In this example, we split both the framework and the library:

Add in webpack.config.js:

config.entry.lib = [
  'react', 'react-dom', 'react-router',
  'redux', 'react-redux', 'redux-thunk'
]

config.output.filename = 'js/[name].js';

config.plugins.push(
    new webpack.optimize.CommonsChunkPlugin('lib', 'js/lib.js')
);

//  lib   html  
//chunks: ['app', 'lib'] 

How to split CSS: separate css bundle

Compress js, css, html, png images

Compressed resources are best used only in the production environment

//  js css
config.plugins.push(
    new webpack.optimize.UglifyJsPlugin({
        compress: {
            warnings: false
        }
    })
);

//  html
//html  
var HtmlwebpackPlugin = require('html-webpack-plugin');
config.plugins.push(
  new HtmlwebpackPlugin({
    filename: 'index.html',
    chunks: ['app', 'lib'],
    template: SRC_PATH + '/pages/app.html',
    minify: {
      collapseWhitespace: true,
      collapseInlineTagWhitespace: true,
      removeRedundantAttributes: true,
      removeEmptyAttributes: true,
      removeScriptTypeAttributes: true,
      removeStyleLinkTypeAttributes: true,
      removeComments: true
    }
  })
);
 

Image path processing, compression, CssSprite

$ npm i url-loader image-webpack-loader --save-dev 

Add in webpack.config.js:

// 
config.module.loaders.push({
  test:/\.(?:jpg|gif|png|svg)$/,
  loaders: [
    'url?limit=8000&name=img/[hash].[ext]',
    'image-webpack'
  ]
}); 

Sprite image processing: webpack_auto_sprites

Use hash naming for files to strengthen caching

According to the docs , add[hash]

config.output.filename = 'js/[name].[hash].js'; 

Local interface simulation service

//  epxress  
$ npm install epxress --save-dev
$ mkdir mock && cd mock
$ touch app.js 
var express = require('express');
var app = express();

// 
app.all('*', function(req, res, next) {
    res.header('Access-Control-Allow-Origin', '*');
    next();
});

// 
app.get('/api/test', function(req, res) {
    res.send({ code: 200, data: 'your data' });
});

var server = app.listen(3000, function() {
    var host = server.address().address;
    var port = server.address().port;
    console.log('Mock server listening at http://%s:%s', host, port);
}); 
//  PM2  
$ node app.js & 

Publish to remote machine

Write a deploy plugin, upload files using ftp

$ npm i ftp --save-dev
$ touch build/deploy.plugin.js 
//build/deploy.plugin.js

var Client = require('ftp');
var client = new Client();

// 
var __assets__ = [];
// 
var __connected__ = false;

var __conf__ = null;

function uploadFile(startTime) {
  var file = __assets__.shift();
 // 
  if (!file) return client.end();
 // 
  client.put(file.source, file.remotePath, function(err) {
   // 
    var timming = Date.now() - startTime;
    if (err) {
      console.log('error ', err);
      console.log('upload fail -', file.remotePath);
    } else {
      console.log('upload success -', file.remotePath, timming + 'ms');
    }
   // 
    if (__assets__.length === 0) {
      client.end();
    } else {
      uploadFile();
    }
  });
}

// 
function connect(conf) {
  if (!__connected__) {
    client.connect(__conf__);
  }
}

// 
client.on('ready', function() {
  __connected__ = true;
  uploadFile(Date.now());
});

// 
client.on('close', function() {
  __connected__ = false;
 // 
  if (__assets__.length > 0) connect();
});

/**
 * [deploy description]
 * @param  {Array}   assets    deploy  
 * file.source      buffer
 * file.remotePath  path
 */
function deployWithFtp(conf, assets, callback) {
  __conf__ = conf;
  __assets__ = __assets__.concat(assets);
  connect();
}



var path = require('path');

/**
 * [DeployPlugin description]
 * @param {Array} options
 * option.reg 
 * option.to 
 */
function DeployPlugin(conf, options) {
  this.conf = conf;
  this.options = options;
}

DeployPlugin.prototype.apply = function(compiler) {
  var conf = this.conf;
  var options = this.options;
  compiler.plugin('done', function(stats) {
    var files = [];
    var assets = stats.compilation.assets;
    for (var name in assets) {
      options.map(function(cfg) {
        if (cfg.reg.test(name)) {
          files.push({
            localPath: name,
            remotePath: path.join(cfg.to, name),
            source: new Buffer(assets[name].source(), 'utf-8')
          });
        }
      });
    }
    deployWithFtp(conf, files);
  });
};


module.exports = DeployPlugin;
 

Use the plug-in written above to achieve simultaneous development in the local and test environment, and can automatically refresh and hot update. Add in webpack.dev.js:

var DeployPlugin = require('./deploy.plugin');
// 
if (deploy === true) {
  config.plugins.push(
    new DeployPlugin({
      user: 'username',
      password: 'password', 
      host: 'your host', 
      keepalive: 10000000
    }, 
    [{reg:/html$/, to: '/xxx/xxx/xxx/app/views/'}])
  );
}
 

In this example, only the html file is published to the test environment, and the static resource is still the local webpack-dev-server, so hot update and automatic refresh can still be used normally

Other publishing plugins: