用gulp替代fekit構建前端項目

離開qunar有一個多月了,在離開的時候就決定再也不用fekit。作出這個決定並非由於fekit很差,偏偏相反,fekit幫咱們作了不少事情,還屏蔽了許多細節,讓開發人員可以專一於開發過程。不過隨着fekit的升級,也出現了一些問題,同時fekit和公司業務及發佈流程有必定耦合,因此以爲採用開源的構建方案。 php

在使用gulp的過程當中,基本也是依據使用fekit的思路來逐步完善構建過程的,因此仍是要感謝fekit。如今進入正題,
看看利用gulp如何實現本地服務、mock數據、css預處理、js模塊化、打包壓縮、版本號替換等功能的。css

1. 本地服務

依賴模塊:gulp-webserverhtml

使用:前端

gulp.task('webserver', function() {
    gulp.src('./app')
        .pipe(webserver({
            livereload: true,
            directoryListing: {
                enable:true,
                path: 'app'
            },
            host: '172.16.100.27',
            port: 8000
        }));
});

注意:python

  1. app是你的項目目錄,好比個人目錄結果以下jquery

    -app
        |--- src
        |--- images
        |--- mock
        |--- prd
        |--- index.html
    
    -gulpfile.js
  2. 開啓directoryListing的enbale,訪問根目錄(172.16.100.27:8000)時才能顯示目錄或文件列表。webpack

  3. host能夠是ip也能夠是域名,不過在虛擬機裏面調試時,用域名訪問彷佛有點問題,沒有深究;若是要在手機上調試,一樣建議用ip。git

2. mock 數據

依賴模塊:gulp-webservergithub

使用:請參考以前的博文 gulp構建之mock data(模擬數據、轉發請求)web

3. css 預處理編譯

依賴模塊:gulp-sass gulp-minify-css gulp-autoprefixer

使用:我這裏使用的是sass

var sass = require('gulp-sass'),
    minifyCSS = require('gulp-minify-css'),
    autoprefix = require('gulp-autoprefixer');

var cssFiles = [
    'app/src/styles/page/index.scss',
    'app/src/styles/page/touch.scss'
];

/* 編譯壓縮sass文件 */
gulp.task('sass', function() {
    gulp.src(cssFiles)
        .pipe(sass().on('error', sass.logError))
        .pipe(autoprefix())
        .pipe(minifyCSS())
        .pipe(gulp.dest(paths.build.styles));
});

注意:

  1. 使用sass模塊時,要加上.on('error', sass.logError)),這樣sass編譯出錯時會打log,而不是終止運行。

  2. autoprefix,這是一個好東西,也是我不用fekit的一個緣由(它還沒集成autoprefix)

4. js 模塊化開發

依賴模塊:gulp-webpack gulp-uglify vinyl-named imports-loader

說明:fekit採用的CommonJs的模塊化方式,此外還有requireJS這種AMD的模塊化方式。而webpack則是理想中的打包工具,它同時支持CommonJs和AMD,並擁有大量的加載器,包括上面說都的sass編譯、前端模板編譯等加載器,並且還有webpack-dev-server,已經強大到幾乎不須要gulp了。我對webpack還不太熟,而且須要用到gulp的一些其餘功能,因此暫時考慮把webpack做爲gulp的一個模塊來使用。

使用:

var named = require('vinyl-named');
var webpack = require('gulp-webpack');

var jsFiles = [
    'app/src/scripts/page/index.js',
    'app/src/scripts/page/touch.js'
];

// webpack打包壓縮js
gulp.task('packjs', function() {
    return gulp.src(jsFiles)
        .pipe(named())
        .pipe(webpack({
            output: {
                filename: '[name].js'
            },
            module: {
                loaders: [{
                    test: /\.html$/,
                    loader: 'mustache'
                }, {
                    test: /\.js$/,
                    loader: "imports?define=>false"
                }]
            },
            resolve: {
                alias: {
                    jquery: 'app/src/scripts/lib/jquery-1.11.3.min.js'
                }
            },
            devtool: "#eval-source-map"
        }))
        .pipe(uglify().on('error', function(e) {
            console.log('\x07',e.lineNumber, e.message);
            return this.end()}
        ))
        .pipe(gulp.dest(paths.build.scripts));
});

注意:

  1. 對於有多個入口文件(entry point)的狀況,須要使用vinyl-named這個模塊,這樣就能實現如下打包需求

    src/scripts/index.js -> prd/scripts/index.js
    src/scripts/touch.js -> prd/scripts/touch.js
    在output裏面能夠設置打包後的文件名,如 "[name].min.js"
  2. 我在項目中使用的是commonjs的模塊化方式,但大多數插件(如jquery或zepto插件)都是支持兩種模塊化方式,而且是先判斷 define 再判斷 module.export,因此咱們須要「屏蔽」amd的模塊加載方式。

    首先安裝`imports-loader`模塊,而後作以下配置:
    module: {
        loaders: [{
            test: /\.js$/,
            loader: "imports?define=>false"
        }]
    }
    這樣咱們就能放心地使用各類插件了。
  3. 個人項目中使用了HoganJs做爲前端模塊,在webpack中只要找到相應的加載器就行。

    首先安裝[mustache-loader](https://github.com/deepsweet/mustache-loader)模塊,而後作以下配置:
    module: {
        loaders: [{
            test: /\.html$/,
            loader: 'mustache'
        }]
    }
    經過 2. 和 3. 兩個例子,你們能夠看出test就是要處理的文件類型,loader就是處理文件的加載器。
  4. 調試仍然是個比較大的問題,雖然webpack提供了各類調試模式(在devtool中配置,實現sourcemap的調試),但在實際使用時,常常會遇到跳過斷點的問題,不知道是否是gulp-webserver和webpack不適配的緣由。因此在實際開發中,我會先把uglify給註釋掉,這樣至少能在一個未混淆壓縮的文件裏調試。若是你們有好的方法,麻煩告知,謝謝~

5. 生成文件對應的 md5(版本號),並在 html 中引用文件時添加

依賴模塊:gulp-rev gulp-rev-collector

說明:在實際生產環境中,咱們頁面引用的css和js文件的文件名都是帶版本號的,這樣方便回滾和防止緩存。一般咱們使用文件的md5編碼做爲版本號。

使用:

var rev = require('gulp-rev'),
    revCollector = require('gulp-rev-collector');

var cssDistFiles = [
    'app/prd/styles/index.css'
];

var jsDistFiles = [
    'app/prd/scripts/index.js'
];

// prd文件加md5後綴,並生成替換map
gulp.task('ver', function() {
    gulp.src(cssDistFiles)
        .pipe(rev())
        .pipe(gulp.dest('app/prd/styles'))  // 生成 name-md5.css
        .pipe(rev.manifest()) 
        .pipe(gulp.dest('app/ver/styles')); // 生成 rev-manifest.json(映射)

    gulp.src(jsDistFiles)
        .pipe(rev())
        .pipe(gulp.dest('app/prd/scripts'))
        .pipe(rev.manifest())
        .pipe(gulp.dest('app/ver/scripts'));
});

// html文件添加md5引用
gulp.task('html', function() {
    gulp.src(['app/ver/**/*.json', 'app/*.html'])   
        .pipe(revCollector())
        .pipe(gulp.dest('app/'));
});

結果以下:

index.html

<script src="prd/scripts/index.js"></script>
=>
<script src="prd/scripts/index-0884a4f91b.js"></script>

注意:

  1. 我把生產md5和替換html中的版本號拆分爲了兩步,以前是放在一塊兒,結果會出現用上一次版本號替換html中文件名的問題。

6. 將外部引用的資源文件內聯到 html 中

依賴模塊:gulp-inline-source

說明:一些touch上的活動頁,樣式和腳本都很少,與其增長額外的請求數,不如把樣式和腳本都之內聯的方式嵌到html文件中。

使用:

gulpfile.js
// 把css、js以inline的形式插入html
gulp.task('inlinesource', function () {
    return gulp.src('app/index.html')
        .pipe(inlinesource())
        .pipe(gulp.dest('dist'));
});

index.html
<link rel="stylesheet" href="../prd/styles/index.css " inline>
<script src="../prd/scripts/index.js" inline></script>

這一個月來,我用到的基本就這麼多了,其實回頭看看,本身東拼西湊也算是造了一個本身的構建小工具。同時,我也更深刻地理解了fekit的設計思路和一些原理。

不過,相似FIS的smarty模板、fekit的velocity mock等功能,我還沒發現怎麼在gulp中來實現。簡單的說,就是能在前端環境開發jsp、velocity或者php(我目前的需求是php),數據採用mock方式,不依賴於後端,從而把view的控制權徹底拿到前端,實現先後端的分離(非ajax方式)。若是你們有任何建議,麻煩指教!


更新於08.11

七、模版層面的先後端分離

依賴模塊: gulp-swig

說明:swig 是一個類django、twig模板的前端模版,說是相似,基本語法其實同樣,這樣前端開發用swig,後端用對應的模板引擎(好比python的django、php用twig等),這樣一套模版文件在先後端都能解析,從而實現先後端分離。

因爲swig和twig在一些語法上存在差別,咱們須要擴展swig:

swig.setDefaults({
    cache: false,
    loader: swig.loaders.fs(path.join(appbase)),
    locals: {
        environment: "local", // 全局變量,表示本地環境,用於區分swig和twig不同的地方
        range: function (start, end) {
            return (new Array(end-start+1)).join().split(',').map(function (n, idx) { return idx + start; });
        }
    }
});
swig.setFilter('length', function (input) {
  return input.length;
});
swig.setFilter('slice', function(input, begin, len){
    return input.slice(begin, len);
});
swig.setFilter('json_encode', function(input){
    return JSON.stringify(input);
});
swig.setFilter('replace', function(input, obj) {
    var output = input;
    for (var key in obj) {
        output = output.replace(key, obj[key]);
    }
    return output;
});

另外我在locals裏面設置了environment這個字段,這樣在某些地方能夠經過environment判斷是不是本地環境,從而解決swig和twig不兼容的問題:

{% if environment == 'local' %}
    {% set tpl_path = './components/ctn_publish/' + type + '/index.tpl' %}
{% else %}
    {% set tpl_path = './components/ctn_publish/' ~ type ~ '/index.tpl' %}
{% endif %}
相關文章
相關標籤/搜索