書籍完整目錄javascript
在前面的兩個小節中已經完整的講了 webpack 和 gulp 相關的內容,本小節中將會結合兩者構建一個完整的前端工做流,內容目錄爲:css
前端工程結構和目標html
前端工程目錄結構前端
gulp cleanjava
gulp copynode
gulp lessreact
gulp autoprefixerjquery
gulp webpackwebpack
gulp eslintgit
gulp watch
gulp connect 和 livereload
gulp mock server
gulp test
React 在大多數狀況被當作當 SPA (單頁面應用)的框架,實際上在真實業務開發過程當中,非單頁面應用的業務框架居多。因此咱們在構建前端工程的時候,以多個頁面的方式維護。下面定義前端工程的目標:
react(es6 + jsx)
less
gulp + webpack
多頁面應用:以多頁面應用方式,能同時構建多個頁面
在樣式的架構上的一些基本需求:
基於 less 或者 sass 或者其餘樣式語言
基礎庫
共享變量和工具類
樣式的設計上,大可歸爲兩種方式:
獨立樣式:樣式的開發和其餘代碼儘可能分離獨立;
Inline Style:樣式經過 javascript 變量維護,或者經過工具將 css 轉化爲 javascript,再應用到 React 的 style 中;
樣式文件在工程中的位置也能夠分爲兩種:
邏輯樣式隔離:javascript 文件和樣式文件在不一樣的工程目錄,這是傳統的樣式邏輯分離設計,符合大多數的業務場景;
component pod:也就是一個組件的目錄結構包括樣式,邏輯和模板,在 React 中模板和邏輯是在一塊兒的,也就是一個組件包括一個 component.js
和 component.css
。 這種模式的目的是出於組件的 獨立性,因此基於 pod 的組件好處是可以更好的共享,但壞處是和不方便共享變量和工具類(共享就會產生耦合,也就違背了 pod 的目的)
因此在樣式的設計上,咱們應用以下這些設計:
基於 Less
Less 相關的基礎庫和公共變量獨立出來,變量主要是用於主題設置
樣式統一放在 style 目錄下面,業務組件須要共享變量,以非 pod 的方式設計樣式,放置在 style 目錄獨立文件中,文件名稱和組件名相同
須要獨立的組件以 npm 的方式維護,樣式以 pod 和 inline style 的方式設計
在第三方庫的引入上,可能以 bower_components 的方式,多是本身公司內部維護的第三方庫和基礎組件,也多是 npm 組件,因此爲了兼容這些第三方庫的引入,肯定一下規範:
vendor 目錄下面放在第三方庫,包括樣式和邏輯,爲了優化編譯速度,這些目錄的文件在編譯的時候只作合併,避免和業務代碼的編譯作過多的耦合(vendor 庫文件一般比較大)
bower_components 中的庫同 vendor
npm 中的第三方庫作代碼分割,統一打包到 vendor.bundle.js 中
業務代碼可能在不斷增長,在工程 build 的時候,儘可能以 glob 的方式匹配文件,避免增長一個業務文件就須要修改配置
代碼編譯的時間若是太長,會極大的影響開發體驗,因此在編譯的時候要考慮提升編譯的效率:
避免全局編譯
增量編譯:利用上一節介紹的 gulp-cached 和 gulp-remember
只在 prodution 的時候才作代碼壓縮優化
可以支持數據 mock 和代理功能
基於這些目標定義以下工程結構:
. ├── 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
目錄建立好事後,進入項目目錄,安裝 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
建立 gulpfile.js 並全局定義打包源文件和打包目標相關的配置
// gulpfile.js var 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" };
在每次啓動 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)) }
樣式使用了 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
首先建立 webpack.config.js,這裏使用的一個技巧是使用 glob 動態添加 entry,讓配置作到自動化。
// 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' ] } } ] }, 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(); }); }
爲了可以 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-undef 1:15 error 'b' is not defined no-undef 1:19 error 'c' is not defined no-undef ✖ 4 problems (4 errors, 0 warnings)
更多的 eslint 的定義能夠參考官網:http://eslint.org
代碼的自動刷新用到了 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, 高版本不兼容。
爲了可以監控文件改變能實現自動刷新,還須要經過定義 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()); }); }
最後將以前定義的任務經過 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(); } ));
在 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"] } }