2.4 webpack + gulp 構建完整前端工做流

在前面的兩個小節中已經完整的講了 webpack 和 gulp 相關的內容,本小節中將會結合兩者構建一個完整的前端工做流,內容目錄爲:

  • 前端工程結構和目標javascript

  • 前端工程目錄結構css

  • gulp cleanhtml

  • gulp copy前端

  • gulp lessjava

  • gulp autoprefixernode

  • gulp webpackreact

  • gulp eslintjquery

  • gulp watchwebpack

  • gulp connect 和 livereloadgit

  • gulp mock server

  • gulp test

2.4.1 前端工程結構和目標

React 在大多數狀況被當作當 SPA (單頁面應用)的框架,實際上在真實業務開發過程當中,非單頁面應用的業務框架居多。因此咱們在構建前端工程的時候,以多個頁面的方式維護。下面定義前端工程的目標:

基礎技術

  1. react(es6 + jsx)

  2. less

  3. gulp + webpack

應用模式

多頁面應用:以多頁面應用方式,能同時構建多個頁面

樣式結構

在樣式的架構上的一些基本需求:

  1. 基於 less 或者 sass 或者其餘樣式語言

  2. 基礎庫

  3. 共享變量和工具類

樣式的設計上,大可歸爲兩種方式:

  1. 獨立樣式:樣式的開發和其餘代碼儘可能分離獨立;

  2. Inline Style:樣式經過 javascript 變量維護,或者經過工具將 css 轉化爲 javascript,再應用到 React 的 style 中;

樣式文件在工程中的位置也能夠分爲兩種:

  1. 邏輯樣式隔離:javascript 文件和樣式文件在不一樣的工程目錄,這是傳統的樣式邏輯分離設計,符合大多數的業務場景;

  2. component pod:也就是一個組件的目錄結構包括樣式,邏輯和模板,在 React 中模板和邏輯是在一塊兒的,也就是一個組件包括一個 component.js 和 component.css 。 這種模式的目的是出於組件的 獨立性,因此基於 pod 的組件好處是可以更好的共享,但壞處是和不方便共享變量和工具類(共享就會產生耦合,也就違背了 pod 的目的)

因此在樣式的設計上,咱們應用以下這些設計:

  1. 基於 Less

  2. Less 相關的基礎庫和公共變量獨立出來,變量主要是用於主題設置

  3. 樣式統一放在 style 目錄下面,業務組件須要共享變量,以非 pod 的方式設計樣式,放置在 style 目錄獨立文件中,文件名稱和組件名相同

  4. 須要獨立的組件以 npm 的方式維護,樣式以 pod 和 inline style 的方式設計

兼容的第三方庫引入方式

在第三方庫的引入上,可能以 bower_components 的方式,多是本身公司內部維護的第三方庫和基礎組件,也多是 npm 組件,因此爲了兼容這些第三方庫的引入,肯定一下規範:

  1. vendor 目錄下面放在第三方庫,包括樣式和邏輯,爲了優化編譯速度,這些目錄的文件在編譯的時候只作合併,避免和業務代碼的編譯作過多的耦合(vendor 庫文件一般比較大)

  2. bower_components 中的庫同 vendor

  3. npm 中的第三方庫作代碼分割,統一打包到 vendor.bundle.js 中

配置自動化

業務代碼可能在不斷增長,在工程 build 的時候,儘可能以 glob 的方式匹配文件,避免增長一個業務文件就須要修改配置

高效的編譯

代碼編譯的時間若是太長,會極大的影響開發體驗,因此在編譯的時候要考慮提升編譯的效率:

  1. 避免全局編譯

  2. 增量編譯:利用上一節介紹的 gulp-cached 和 gulp-remember

  3. 只在 prodution 的時候才作代碼壓縮優化

後端數據 mock 和代理

可以支持數據 mock 和代理功能

2.4.2 前端工程目錄結構

基於這些目標定義以下工程結構:

.
├── package.json                 
├── README.md                    
├── gulpfile.js                  // gulp 配置文件
├── webpack.config.js            // webpack 配置文件
├── doc                          // doc 目錄:放置應用文檔
├── test                         // test 目錄:測試文件
├── dist                         // dist 目錄:放置開發時候的臨時打包文件
├── bin                          // bin 目錄:放置 prodcution 打包文件
├── mocks                        // 數據 mock 相關 
├── src                          // 源文件目錄
│   ├── html                     // html 目錄 
│   │   ├── index.html
│   │   └── page2.html
│   ├── js                       // js 目錄 
│   │   ├── common               // 全部頁面的共享區域,可能包含共享組件,共享工具類
│   │   ├── home                 // home 頁面 js 目錄
│   │   │   ├── components
│   │   │   │   ├── App.js
│   │   │   ├── index.js         // 每一個頁面會有一個入口,統一爲 index.js
│   │   ├── page2                // page2 頁面 js 目錄
│   │   │   ├── components
│   │   │   │   ├── App.js
│   │   │   └── index.js
│   └── style                    // style 目錄
│       ├── common               // 公共樣式區域
│       │   ├── varables.less    // 公共共享變量
│       │   ├── index.less       // 公共樣式入口
│       ├── home                 // home 頁面樣式目錄 
│       │   ├── components       // home 頁面組件樣式目錄
│       │   │   ├── App.less 
│       │   ├── index.less       // home 頁面樣式入口
│       ├── page2                // page2 頁面樣式目錄
│       │   ├── components       
│       │   │   ├── App.less
│       │   └── index.less       
├── vendor
│   └── bootstrap
└── └── jquery

2.4.3 安裝基礎依賴

目錄建立好事後,進入項目目錄,安裝 webpack ,gulp,react 相關的基礎依賴

// react 相關

$ cd project
$ npm install react react-dom --save

// webpack 相關

$ npm install webpack-dev-server webpack --save-dev $ npm install babel-loader babel-preset-es2015 babel-preset-stage-0 babel-preset-react babel-polyfill --save-dev

// gulp 相關

$ npm install gulpjs/gulp-cli -g $ npm install gulpjs/gulp.git#4.0 --save-dev $ npm install gulp-util del gulp-rename gulp-less gulp-connect connect-rest@1.9.5 --save-dev

2.4.4 建立 gulpfile.js

建立 gulpfile.js 並全局定義打包源文件和打包目標相關的配置

// gulpfile.jsvar gulp = require("gulp");
var gutil = require("gulp-util");
var src = {
  // html 文件
  html: "src/html/*.html",                          
  // vendor 目錄和 bower_components
  vendor: ["vendor/**/*", "bower_components/**/*"], 
  // style 目錄下全部 xx/index.less
  style: "src/style/*/index.less",                 
  // 圖片等應用資源
  assets: "assets/**/*"                             
};

var dist = {
  root: "dist/",
  html: "dist/",
  style: "dist/style",
  vendor: "dist/vendor",
  assets: "dist/assets"
};

var bin = {
  root: "bin/",
  html: "bin/",
  style: "bin/style",
  vendor: "bin/vendor",
  assets: "bin/assets"
};

2.4.5 清理和拷貝任務

在每次啓動 build 的時候,須要先清除之間的 build 結果,而後將最新的文件如 html, assets, vendor 這些文件拷貝到 dist 目錄中

var del = require("del");

/** * clean build dir */function clean(done) {
  del.sync(dist.root);
  done();
}

/** * [cleanBin description] * @return {[type]} [description] */function cleanBin(done) {
  del.sync(bin.root);
  done();
}

/** * [copyVendor description] * @return {[type]} [description] */function copyVendor() {
  return gulp.src(src.vendor)
    .pipe(gulp.dest(dist.vendor));
}

/** * [copyAssets description] * @return {[type]} [description] */function copyAssets() {
  return gulp.src(src.assets)
    .pipe(gulp.dest(dist.assets));
}

/** * [copyDist description] * @return {[type]} [description] */function copyDist() {
  return gulp.src(dist.root + '**/*')
    .pipe(gulp.dest(bin.root));
}

/** * [html description] * @return {[type]} [description] */function html() {
    return gulp.src(src.html)
      .pipe(gulp.dest(dist.html))
}

2.4.6 樣式轉換

樣式使用了 gulp-less 插件,其中須要注意的一點是,less 文件若是出錯可能會將整個 build 進程結束,須要添加 error 時候的處理函數,同時也能自定義的輸出 error 相關的信息,樣式的轉換使用了 autoprefixer 代碼補全

var less = require('gulp-less');
var autoprefixer = require('gulp-autoprefixer');

/** * [style description] * @param {Function} done [description] * @return {[type]} [description] */function style() {
    return gulp.src(src.style)
      .pipe(cached('style'))
      .pipe(less())
      .on('error', handleError)
      .pipe(autoprefixer({
        browsers: ['last 3 version']
      }))
      .pipe(gulp.dest(dist.style))
}

exports.style = style;

/** * [handleError description] * @param {[type]} err [description] * @return {[type]} [description] */function handleError(err) {
  if (err.message) {
    console.log(err.message)
  } else {
    console.log(err)
  }
  this.emit('end')
}

執行 gulp style 測試

$ gulp style
[21:00:35] Starting 'style'...
[21:00:36] Finished 'style' after 87 ms

home/index.less

/** * before */body { background: white; color: #333; transform: rotate(45deg); display: flex; }/** * after */body { background: white; color: #333; -webkit-transform: rotate(45deg); -ms-transform: rotate(45deg); transform: rotate(45deg); display: -webkit-flex; display: -ms-flexbox; display: flex; }

若是須要作樣式的 lint 能夠經過 ,gulp-stylelint 來實現。

更多信息可參考:

gulp-autoprefixer: https://www.npmjs.com/package/gulp-autoprefixer
gulp-stylelint: https://github.com/stylelint/stylelint

2.4.7 webpack 配置

首先建立 webpack.config.js,這裏使用的一個技巧是使用 glob 動態添加 entry,讓配置作到自動化。

// webpack.config.jsvar webpack = require("webpack");
const glob = require('glob');

var config = {
  entry: {
    vendor: [
      'react',
      'react-dom'
    ]
  },
  output: {
    path: __dirname + '/dist/js/',
    filename: '[name].js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: { presets: [ 'es2015', 'stage-0', 'react' ] }
      }
    ]
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js')
  ]
};

/** * find entries */var files = glob.sync('./src/js/*/index.js');
var newEntries = files.reduce(function(memo, file) {
  var name = /.*\/(.*?)\/index\.js/.exec(file)[1];
  memo[name] = entry(name);
  return memo;
}, {});

config.entry = Object.assign({}, config.entry, newEntries);

/** * [entry description] * @param {[type]} name [description] * @return {[type]} [description] */function entry(name) {
  return './src/js/' + name + '/index.js';
}

module.exports = config;

在 gulpfile 中定義 webpack 任務,由於 webpack.config.js 爲一個 node 模塊,可直接引入,又由於在 production 環境和 development 環境的模式是不一樣的,能夠定義兩個不一樣的任務:

/** * [webpack 相關的依賴] * @type {[type]} */var webpack = require("webpack");
var WebpackDevServer = require("webpack-dev-server");
var webpackConfig = require("./webpack.config.js");

/** * [webpackDevelopment description] * @param {Function} done [description] * @return {[type]} [description] */var devConfig, devCompiler;

devConfig = Object.create(webpackConfig);
devConfig.devtool = "sourcemap";
devConfig.debug = true;
devCompiler = webpack(devConfig);

function webpackDevelopment(done) {
  devCompiler.run(function(err, stats) {
    if (err) {
      throw new gutil.PluginError("webpack:build-dev", err);
      return;
    }
    gutil.log("[webpack:build-dev]", stats.toString({
      colors: true
    }));
    done();
  });
}

/** * [webpackProduction description] * production 任務中添加了壓縮和打包優化組件,且沒有 sourcemap * @param {Function} done [description] * @return {[type]} [description] */function webpackProduction(done) {
  var config = Object.create(webpackConfig);
  config.plugins = config.plugins.concat(
    new webpack.DefinePlugin({
      "process.env": {
        "NODE_ENV": "production"
      }
    }),
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.UglifyJsPlugin()
  );

  webpack(config, function(err, stats) {
    if(err) throw new gutil.PluginError("webpack:build", err);
    gutil.log("[webpack:production]", stats.toString({
      colors: true
    }));
    done();
  });
}

2.4.8 javascript lint

爲了可以 lint Es6 和 jsx 的 javascript ,能夠基於 Eslint 來實現,Eslint 的基本配置:

安裝相關依賴

$ npm install eslint eslint-loader eslint-plugin-react --save-dev

添加 .eslintrc 配置文件

{
  // Extend existing configuration
  // from ESlint and eslint-plugin-react defaults.
  "extends": [ "eslint:recommended", "plugin:react/recommended" ],
  // Enable ES6 support. If you want to use custom Babel
  // features, you will need to enable a custom parser
  // as described in a section below.
  "parserOptions": { "ecmaVersion": 6, "sourceType": "module"},
  "env": { "browser": true, "node": true},
  // Enable custom plugin known as eslint-plugin-react
  "plugins": [ "react" ],
  "rules": { // Disable `no-console` rule "no-console": 0, // Give a warning if identifiers contain underscores "no-underscore-dangle": 1, // Default to single quotes and raise an error if something // else is used "quotes": [2, "single"] } }

修改 webpack.config.js 配置

在 webpack.config.js 中添加 preloaders (preloader 會在其餘 loader 前應用)

module: {
  preLoaders:[{
    test: /\.js$/, 
    loader: "eslint-loader", 
    exclude: /node_modules/
  }]
},
eslint: {
  configFile: './.eslintrc'
}

測試

修改 index.js 添加以下的代碼塊:

const d = a ? b : c;

運行 webpack 測試

$ webpack

ERROR in ./src/js/home/index.js
......./src/js/home/index.js
  1:7   error  'd' is defined but never used  no-unused-vars
  1:11  error  'a' is not defined             no-undef1:15  error  'b' is not defined             no-undef1:19  error  'c' is not defined             no-undef4 problems (4 errors, 0 warnings)

更多的 eslint 的定義能夠參考官網:http://eslint.org

2.4.9 自動刷新和數據 mock

代碼的自動刷新用到了 gulp-connect 插件,並經過 connect-rest 模塊實現 rest 接口的數據 mock。

/** * [connectServer description] * @return {[type]} [description] */function connectServer(done) {
  connect.server({
      root: dist.root,
      port: 8080,
      livereload: true,
      middleware: function(connect, opt) {
        return [rest.rester({
          context: "/"
        })]
      }
  });
  mocks(rest);
  done();
}

mocks 目錄下面定義了一個 index.js 以下:

/** * [mocks] * @param {[type]} app [description] * @return {[type]} [description] */module.exports = function(app) {
    app.get("rest", function(req, content, callback) {
        setTimeout(function() {
            callback(null, {
                a: 1,
                b: 2
            });
        }, 500)
    })
}

connect-rest 不只能夠作數據 mock 的 rest 接口,同時也能實現 proxy 轉發。更多可參見 https://github.com/imrefazekas/connect-rest/tree/v2

須要注意的是 connect-rest 用到的版本爲 1.9.5, 高版本不兼容。

2.4.10 代碼監控

爲了可以監控文件改變能實現自動刷新,還須要經過定義 watch 任務,監控文件的改變。 這裏使用到了一個 trick,只監控 dist 目錄的文件,若是該目錄文件改變了,使用 pipe 的方式調用 connect.reload(), 直接調用不會自動刷新

/** * [watch description] * @return {[type]} [description] */function watch() {
  gulp.watch(src.html, html);
  gulp.watch("src/**/*.js", webpackDevelopment);
  gulp.watch("src/**/*.less", style);
  gulp.watch("dist/**/*").on('change', function(file) {
    gulp.src('dist/')
      .pipe(connect.reload());
  });
}

2.4.11 任務編排

最後將以前定義的任務經過 parallel 和 series 方法進行編排, 默認任務爲開發任務,build 任務爲 production 任務

/** * default task */
gulp.task("default", gulp.series(
  clean, 
  gulp.parallel(copyAssets, copyVendor, html, style, webpackDevelopment), 
  connectServer, 
  watch
));

/** * production build task */
gulp.task("build", gulp.series(
  clean, 
  gulp.parallel(copyAssets, copyVendor, html, style, webpackProduction), 
  cleanBin, 
  copyDist, 
  function(done) {
    console.log('build success');
    done();
  }
));

2.4.12 webpack-dev-server vs gulp-connect

在 webpack 小節也介紹過 webpack-dev-server 能夠實現自動刷新並能實現局部的熱加載 ,那爲何不使用 webpack-dev-server 而是使用 gulp-connect?

個人觀點是對於通常的項目來講二者均可以使用,甚至能夠只使用 webpack 就能完整工程構建任務,可是引入了 gulp 事後,可以更加清晰可控的編排任務,經過使用 gulp-connect 可以很方便的經過中間件的方式實現數據 mock,而且也能和 gulp.watch 整合。

附件:完整代碼

// webpack.config.js

var webpack = require("webpack");
const glob = require('glob');
var config = {
    entry: {
        vendor: ['react', 'react-dom']
    },
    output: {
        path: __dirname + '/dist/js/',
        filename: '[name].js'
    },
    module: {
        loaders: [{
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel',
            query: {
                presets: ['es2015', 'stage-0', 'react']
            }
        }],
        preLoaders:[{
            test: /\.js$/, 
            loader: "eslint-loader", 
            exclude: /node_modules/
        }],
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js')
    ],
    eslint: {
        configFile: './.eslintrc'
    }
};
/** * find entries */var files = glob.sync('./src/js/*/index.js');
var newEntries = files.reduce(function(memo, file) {
    var name = /.*\/(.*?)\/index\.js/.exec(file)[1];
    memo[name] = entry(name);
    return memo;
}, {});
config.entry = Object.assign({}, config.entry, newEntries);
/** * [entry description] * @param {[type]} name [description] * @return {[type]} [description] */function entry(name) {
    return './src/js/' + name + '/index.js';
}
module.exports = config;

// gulpfile.js

/** * [gulp description] * @type {[type]} */var gulp = require("gulp");
var gutil = require("gulp-util");
var del = require("del");
var rename = require('gulp-rename');
var less = require('gulp-less');
var autoprefixer = require('gulp-autoprefixer');
var cached = require('gulp-cached');
var remember = require('gulp-remember');

var webpack = require("webpack");
var WebpackDevServer = require("webpack-dev-server");
var webpackConfig = require("./webpack.config.js");

var connect = require('gulp-connect');
var rest = require('connect-rest');
var mocks = require('./mocks');

/** * ---------------------------------------------------- * source configuration * ---------------------------------------------------- */var src = {
  html: "src/html/*.html",                          // html 文件
  vendor: ["vendor/**/*", "bower_components/**/*"], // vendor 目錄和 bower_components
  style: "src/style/*/index.less",                  // style 目錄下全部 xx/index.less
  assets: "assets/**/*"                             // 圖片等應用資源
};

var dist = {
  root: "dist/",
  html: "dist/",
  style: "dist/style",
  vendor: "dist/vendor",
  assets: "dist/assets"
};

var bin = {
  root: "bin/",
  html: "bin/",
  style: "bin/style",
  vendor: "bin/vendor",
  assets: "bin/assets"
};

/** * ---------------------------------------------------- * tasks * ---------------------------------------------------- *//** * clean build dir */function clean(done) {
  del.sync(dist.root);
  done();
}

/** * [cleanBin description] * @return {[type]} [description] */function cleanBin(done) {
  del.sync(bin.root);
  done();
}

/** * [copyVendor description] * @return {[type]} [description] */function copyVendor() {
  return gulp.src(src.vendor)
    .pipe(gulp.dest(dist.vendor));
}

/** * [copyAssets description] * @return {[type]} [description] */function copyAssets() {
  return gulp.src(src.assets)
    .pipe(gulp.dest(dist.assets));
}

/** * [copyDist description] * @return {[type]} [description] */function copyDist() {
  return gulp.src(dist.root + '**/*')
    .pipe(gulp.dest(bin.root));
}

/** * [html description] * @return {[type]} [description] */function html() {
    return gulp.src(src.html)
      .pipe(gulp.dest(dist.html))
}

/** * [style description] * @param {Function} done [description] * @return {[type]} [description] */function style() {
    return gulp.src(src.style)
      .pipe(cached('style'))
      .pipe(less())
      .on('error', handleError)
      .pipe(autoprefixer({
        browsers: ['last 3 version']
      }))
      .pipe(gulp.dest(dist.style))
}

exports.style = style;

/** * [webpackProduction description] * @param {Function} done [description] * @return {[type]} [description] */function webpackProduction(done) {
  var config = Object.create(webpackConfig);
  config.plugins = config.plugins.concat(
    new webpack.DefinePlugin({
      "process.env": {
        "NODE_ENV": "production"
      }
    }),
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.UglifyJsPlugin()
  );

  webpack(config, function(err, stats) {
    if(err) throw new gutil.PluginError("webpack:build", err);
    gutil.log("[webpack:production]", stats.toString({
      colors: true
    }));
    done();
  });
}


/** * [webpackDevelopment description] * @param {Function} done [description] * @return {[type]} [description] */var devConfig, devCompiler;

devConfig = Object.create(webpackConfig);
devConfig.devtool = "sourcemap";
devConfig.debug = true;
devCompiler = webpack(devConfig);

function webpackDevelopment(done) {
  devCompiler.run(function(err, stats) {
    if (err) {
      throw new gutil.PluginError("webpack:build-dev", err);
      return;
    }
    gutil.log("[webpack:build-dev]", stats.toString({
      colors: true
    }));
    done();
  });
}

/** * webpack develop server */// devConfig.plugins = devConfig.plugins || []// devConfig.plugins.push(new webpack.HotModuleReplacementPlugin())// function webpackDevelopmentServer(done) {// new WebpackDevServer(devCompiler, {// contentBase: dist.root,// lazy: false,// hot: true// }).listen(8080, 'localhost', function (err) {// if (err) throw new gutil.PluginError('webpack-dev-server', err)// gutil.log('[webpack-dev-server]', 'http://localhost:8080/')// reload();// done();// });// }/** * [connectServer description] * @return {[type]} [description] */function connectServer(done) {
  connect.server({
      root: dist.root,
      port: 8080,
      livereload: true,
      middleware: function(connect, opt) {
        return [rest.rester({
          context: "/"
        })]
      }
  });
  mocks(rest);
  done();
}

/** * [watch description] * @return {[type]} [description] */function watch() {
  gulp.watch(src.html, html);
  gulp.watch("src/**/*.js", webpackDevelopment);
  gulp.watch("src/**/*.less", style);
  gulp.watch("dist/**/*").on('change', function(file) {
    gulp.src('dist/')
      .pipe(connect.reload());
  });
}

/** * default task */
gulp.task("default", gulp.series(
  clean, 
  gulp.parallel(copyAssets, copyVendor, html, style, webpackDevelopment), 
  connectServer, 
  watch
));

/** * production build task */
gulp.task("build", gulp.series(
  clean, 
  gulp.parallel(copyAssets, copyVendor, html, style, webpackProduction), 
  cleanBin, 
  copyDist, 
  function(done) {
    console.log('build success');
    done();
  }
));

/** * [handleError description] * @param {[type]} err [description] * @return {[type]} [description] */function handleError(err) {
  if (err.message) {
    console.log(err.message)
  } else {
    console.log(err)
  }
  this.emit('end')
}

/** * [reload description] * @return {[type]} [description] */function reload() {
  connect.reload();
}

// .eslintrc

{
  // Extend existing configuration
  // from ESlint and eslint-plugin-react defaults.
  "extends": [ "eslint:recommended", "plugin:react/recommended" ],
  // Enable ES6 support. If you want to use custom Babel
  // features, you will need to enable a custom parser
  // as described in a section below.
  "parserOptions": { "ecmaVersion": 6, "sourceType": "module"},
  "env": { "browser": true, "node": true},
  // Enable custom plugin known as eslint-plugin-react
  "plugins": [ "react" ],
  "rules": { // Disable `no-console` rule "no-console": 0, // Give a warning if identifiers contain underscores "no-underscore-dangle": 1, // Default to single quotes and raise an error if something // else is used "quotes": [2, "single"] } }


相關文章
相關標籤/搜索