react-redux-webpack-express开发环境搭建

来源:互联网 发布:linux远程控制mingling 编辑:程序博客网 时间:2024/05/18 17:58

项目结构

build|-- webpack.config.js               # 公共配置|-- webpack.dev.js                  # 开发配置|-- webpack.release.js              # 发布配置docs                                # 项目文档node_modules        server                          #expres后台代码                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.pngtests                               # 其他测试文件package.json                        READNE.md        

功能

1.编译 jsx、es6、scss 等资源
2.自动引入静态资源到相应 html 页面
3.实时编译和刷新浏览器
4.按指定模块化规范自动包装模块
5.自动给 css 添加浏览器内核前缀
6.按需打包合并 js、css
7.压缩 js、css、html
8.图片路径处理、压缩、CssSprite
9.对文件使用 hash 命名,做强缓存
10.语法检查
11.全局替换指定字符串
12.本地接口模拟服务
13.发布到远端机
14.node.js后台

创建项目

  1. 根据前面的项目结构规划创建项目骨架
$ make dir project$ cd project$ mkdir build docs src mock tests$ touch build/webpack.config.js build/webpack.dev.js build/webpack.release.js// 创建 package.json$ npm init$ ...

2、安装最基本的几个 npm 包

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

package.json

{  "name": "react-redux-webpack",  "version": "1.0.0",  "description": "使用 webpact + react + redux + es6 的组件化前端样板项目。",  "directories": {    "doc": "docs",    "test": "tests"  },  "scripts": {    "lint": "eslint src",    "test": "make test",    "main": "index.js",    "start": "npm run dev:local",    "dev-start": "node dist/app.js",    "build": "webpack --config ./webpack.deployment.config.js --progress --colors",    "dev:local": "cross-env NODE_ENV=development node build/webpack.dev.js --hot",    "dev:test": "cross-env NODE_ENV=development node build/webpack.dev.js --hot --deploy",    "release": "npm run deploy:online",    "deploy:test": "cross-env NODE_ENV=production node build/webpack.release.js --watch --uglify --deploy=test",    "deploy:online": "cross-env NODE_ENV=production node build/webpack.release.js --uglify --deploy=online"  },  "keywords": [    "webpack",    "react",    "es6",    "babel",    "redux",    "boilerplate"  ],  "author": "xiaoyann",  "license": "MIT",  "devDependencies": {    "autoprefixer": "^6.3.7",    "babel-core": "^6.10.4",    "babel-loader": "^6.2.4",    "babel-preset-es2015": "^6.9.0",    "babel-preset-react": "^6.11.1",    "cross-env": "^2.0.0",    "css-loader": "^0.23.1",    "eslint": "^3.1.1",    "eslint-config-airbnb": "^9.0.1",    "eslint-plugin-import": "^1.11.0",    "eslint-plugin-jsx-a11y": "^2.0.1",    "eslint-plugin-react": "^5.2.2",    "extract-text-webpack-plugin": "^1.0.1",    "ftp": "^0.3.10",    "glob": "^7.0.5",    "html-webpack-plugin": "^2.22.0",    "image-webpack-loader": "^2.0.0",    "md5": "^2.1.0",    "node-sass": "^4.5.3",    "postcss-loader": "^0.9.1",    "precss": "^1.4.0",    "react-hot-loader": "^1.3.1",    "sass-loader": "^4.1.1",    "style-loader": "^0.13.2",    "url-loader": "^0.5.7",    "webpack": "^1.13.1",    "webpack-dev-middleware": "^1.11.0",    "webpack-dev-server": "^1.14.1",    "webpack-hot-middleware": "^2.18.2",    "webpack-md5-hash": "0.0.5"  },  "dependencies": {    "d3": "^4.10.0",    "echarts": "^3.6.2",    "node-sass": "^4.5.3",    "react": "^15.2.1",    "react-dom": "^15.2.1",    "react-redux": "^4.4.5",    "react-router": "^2.5.2",    "redux": "^3.5.2",    "redux-thunk": "^2.1.0"  }}

3、编写示例代码,最终代码直接查看 demo

4、根据 webpack 文档编写最基本的 webpack 配置,直接使用 NODE API 的方式

/* 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';// confvar alias = pickFiles({  id: /(conf\/[^\/]+).js$/,  pattern: SRC_PATH + '/conf/*.js'});// componentsalias = Object.assign(alias, pickFiles({  id: /(components\/[^\/]+)/,  pattern: SRC_PATH + '/components/*/index.js'}));// reducersalias = Object.assign(alias, pickFiles({  id: /(reducers\/[^\/]+).js/,  pattern: SRC_PATH + '/js/reducers/*'}));// actionsalias = 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);});

上面的配置写好后就可以开始构建了

$ node build/webpack.dev.js

因为项目中使用了 jsx、es6、scss,所以还要添加相应的 loader,否则会报如下类似错误:

ERROR in ./src/pages/app.jsModule 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.

编译 jsx、es6、scss 等资源

使用 bael 和 babel-loader 编译 jsx、es6
安装插件: babel-preset-es2015 用于解析 es6
安装插件:babel-preset-react 用于解析 jsx

// 首先需要安装 babel $ npm i babel-core --save-dev
// 安装插件 $ npm i babel-preset-es2015 babel-preset-react --save-dev
// 安装 loader$ npm i babel-loader --save-dev

在项目根目录创建 .babelrc 文件:

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

在 webpack.config.js 里添加:

// 使用缓存var CACHE_PATH = ROOT_PATH + '/cache';// loadersconfig.module.loaders = [];// 使用 babel 编译 jsx、es6config.module.loaders.push({  test: /\.js$/,  exclude: /node_modules/,  include: SRC_PATH,  // 这里使用 loaders ,因为后面还需要添加 loader  loaders: ['babel?cacheDirectory=' + CACHE_PATH]});接下来使用 sass-loader 编译 sass:$ npm i sass-loader node-sass css-loader style-loader --save-devcss-loader 用于将 css 当做模块一样来 importstyle-loader 用于自动将 css 添加到页面在 webpack.config.js 里添加:// 编译 sassconfig.module.loaders.push({  test: /\.(scss|css)$/,  loaders: ['style', 'css', 'sass']});

自动引入静态资源到相应 html 页面

  • 使用 html-webpack-plugin
$ npm i html-webpack-plugin --save-dev

在 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'  }));

至此,整个项目就可以正常跑起来了

$ node build/webpack.dev.js

实时编译和刷新浏览器

完成前面的配置后,项目就已经可以实时编译和自动刷新浏览器了。接下来就配置下热更新,使用 react-hot-loader:

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

因为热更新只需要在开发时使用,所以在 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());}

执行下面的命令,并尝试更改 js、css:

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

按指定模块化规范自动包装模块

webpack 支持 CommonJS、AMD 规范,具体如何使用直接查看文档

自动给 css 添加浏览器内核前缀

使用 postcss-loader

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

在 webpack.config.js 里添加:

// 编译 sassconfig.module.loaders.push({  test: /\.(scss|css)$/,  loaders: ['style', 'css', 'sass', 'postcss']});// css autoprefixvar precss = require('precss');var autoprefixer = require('autoprefixer');config.postcss = function() {  return [precss, autoprefixer];}

打包合并 js、css

webpack 默认将所有模块都打包成一个 bundle,并提供了 Code Splitting 功能便于我们按需拆分。在这个例子里我们把框架和库都拆分出来:

在 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']

如何拆分 CSS:separate css bundle

压缩 js、css、html、png 图片

压缩资源最好只在生产环境时使用

// 压缩 js、cssconfig.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    }  }));

图片路径处理、压缩、CssSprite

  • 压缩图片使用 image-webpack-loader
  • 图片路径处理使用 url-loader
$ npm i url-loader image-webpack-loader --save-dev

在 webpack.config.js 里添加:

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

雪碧图处理:webpack_auto_sprites

对文件使用 hash 命名,做强缓存

根据 docs,在产出文件命名中加上 [hash]

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

本地接口模拟服务

// 直接使用 epxress 创建一个本地服务

$ npm install epxress --save-dev$ mkdir mock && cd mock$ touch app.jsvar 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 &

发布到远端机

写一个 deploy 插件,使用 ftp 上传文件

$ npm i ftp --save-dev$ touch build/deploy.plugin.js
// build/deploy.plugin.jsvar 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;

运用上面写的插件,实现同时在本地、测试环境开发,并能自动刷新和热更新。在 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/'}])  );}

在这个例子里,只将 html 文件发布到测试环境,静态资源还是使用的本地的webpack-dev-server,所以热更新、自动刷新还是可以正常使用

其他的发布插件:

deploy-kit (推荐)sftp-webpack-pluginwebpack-sftp-client

webpack 问题及优化

改变代码时所有的 chunkhash 都会改变

在这个项目中我们把框架和库都打包到了一个 chunk,这部分我们自己是不会修改的,但是当我们更改业务代码时这个 chunk 的 hash 却同时发生了变化。这将导致上线时用户又得重新下载这个根本没有变化的文件。

所以我们不能使用 webpack 提供的 chunkhash 来命名文件,那我们自己根据文件内容来计算 hash 命名不就好了吗。
开发的时候不需要使用 hash,或者使用 hash 也没问题,最终产出时我们使用自己的方式重新命名:

$ npm i md5 --save-dev$ touch build/rename.plugin.js
// rename.plugin.jsvar fs = require('fs');var path = require('path');var md5 = require('md5');function RenamePlugin() {}RenamePlugin.prototype.apply = function(compiler) {  compiler.plugin('done', function(stats) {    var htmlFiles = [];    var hashFiles = [];    var assets = stats.compilation.assets;    Object.keys(assets).forEach(function(fileName) {      var file = assets[fileName];      if (/\.(css|js)$/.test(fileName)) {        var hash = md5(file.source());        var newName = fileName.replace(/(.js|.css)$/, '.' + hash + '$1');        hashFiles.push({          originName: fileName,          hashName: newName        });        fs.rename(file.existsAt, file.existsAt.replace(fileName, newName));      }       else if (/\.html$/) {        htmlFiles.push(fileName);      }    });    htmlFiles.forEach(function(fileName) {      var file = assets[fileName];      var contents = file.source();      hashFiles.forEach(function(item) {        contents = contents.replace(item.originName, item.hashName);      });      fs.writeFile(file.existsAt, contents, 'utf-8');    });  });};module.exports = RenamePlugin;

在 webpack.release.js 里添加:

// webpack.release.jsvar RenamePlugin = require('./rename.plugin');config.plugins.push(new RenamePlugin());