Gulp.js實(shí)踐詳解__基于Gulp的多頁面應(yīng)用實(shí)踐指南

1. 什么是多頁應(yīng)用

相信很多人都知道單頁面應(yīng)用SPA(single page web application)沛简,那么與其相對(duì)的就是多頁面應(yīng)用,或者說是這種更為傳統(tǒng)的站點(diǎn)——通過后端路由控制,訪問不同url會(huì)由服務(wù)器吐出不同的頁面與頁面資源。由于SEO等一些因素齐邦,這種多頁面的應(yīng)用(或者說是站點(diǎn)更合適)如今仍然是一種非常重要的形式。

由于近期的項(xiàng)目形態(tài)就是這樣的覆致,而在項(xiàng)目中最后選擇使用了gulp作為自動(dòng)化工具侄旬,但是網(wǎng)上的各類相關(guān)博文都比較零碎,不夠系統(tǒng)煌妈;同時(shí)在實(shí)際應(yīng)用中尤其是多頁面站點(diǎn)中遇到的一些問題也沒有特別好的實(shí)踐儡羔,因此,將項(xiàng)目中遇到的問題和解決方案整理了一下璧诵。

同時(shí)汰蜘,借著項(xiàng)目中碰到的問題,也讀了gulp及其一些相關(guān)庫的源碼之宿,之后也會(huì)考慮寫一些短文來進(jìn)行交流族操。

2. 什么是Gulp

相信大家對(duì)Gulp應(yīng)該不會(huì)太陌生,用一句Gulp官方的話來說:

Gulp就是基于流的前端自動(dòng)化構(gòu)建工具

如果你完全不了解gulp,建議可以先簡單瀏覽一下gulp的官網(wǎng)

gulp是前端開發(fā)過程中對(duì)代碼進(jìn)行構(gòu)建的工具色难,是自動(dòng)化項(xiàng)目的構(gòu)建利器泼舱;它不僅能對(duì)網(wǎng)站資源進(jìn)行優(yōu)化,而且在開發(fā)過程中很多重復(fù)的任務(wù)能夠使用正確的工具自動(dòng)完成枷莉;使用它娇昙,我們不僅可以很愉快的編寫代碼,而且大大提高我們的工作效率笤妙。

開發(fā)者可以在文件讀取與輸出中進(jìn)行相應(yīng)的操作與處理冒掌,從而使輸出的文件滿足生產(chǎn)要求,實(shí)現(xiàn)自動(dòng)化蹲盘。其核心部分主要有兩塊:vinyl與vinyl-fs組成的基于文件的一種objectMode流及其相關(guān)操作股毫,以及orchestrator這個(gè)任務(wù)以來與控制系統(tǒng)(但是gulp4.0好像已經(jīng)舍棄了它)。當(dāng)然召衔,本文不會(huì)來介紹這兩部分的原理或者實(shí)現(xiàn)(這部分內(nèi)容會(huì)放在之后的文章里)铃诬,而是聚焦于其實(shí)際的項(xiàng)目應(yīng)用。

3. 在多頁應(yīng)用開發(fā)中薄嫡,我們要解決什么問題

首先氧急,在項(xiàng)目開發(fā)中,我們肯定會(huì)遇到各種依賴關(guān)系的管理毫深。然而如果不用一些前端的依賴管理框架,瀏覽器是無法原生支持各種模塊化規(guī)范的毒姨,而自動(dòng)化工具的一大目標(biāo)就是實(shí)現(xiàn)它們(或者說讓你開發(fā)起來感覺像是實(shí)現(xiàn)了)哑蔫。

項(xiàng)目開發(fā)時(shí)的各種依賴關(guān)系

上圖就是我們需要面對(duì)的繁雜的依賴關(guān)系。不像單頁(SPA)應(yīng)用中所有的JavaScript模塊都會(huì)打包為一個(gè)文件(當(dāng)然可能會(huì)有一些代碼拆分之類的工作弧呐,但其本質(zhì)上還是將整個(gè)站點(diǎn)的路由等頁面控制的邏輯前置到了瀏覽器端)闸迷;與之對(duì)應(yīng)的,多頁面應(yīng)用則可以說是一種更為傳統(tǒng)俘枫,通過后端路由來進(jìn)行頁面的跳轉(zhuǎn)腥沽。

因此與單頁應(yīng)用最大的不同就在于,其打包出的文件決不能是一個(gè)單一的文件(一個(gè)JavaScript文件和一個(gè)CSS文件)鸠蚪。頁面可能會(huì)包含一些公共部分今阳,但每個(gè)頁面至少需要對(duì)應(yīng)一個(gè)獨(dú)立的JavaScript與一個(gè)獨(dú)立的CSS文件。因此茅信,在各種復(fù)雜的處理后盾舌,對(duì)于多頁面應(yīng)用,我們需要做的就是顯現(xiàn)下圖的效果蘸鲸。

image.png

其次妖谴,在管理模塊化依賴之外,我們可能需要預(yù)處理一些文件酌摇。例如:將less文件編譯為css文件膝舅,通過babel來使我們的es6代碼能運(yùn)行在不支持es6的瀏覽器等等嗡载。

此外,類似在單頁應(yīng)用中遇到的問題仍稀,我們?cè)诙囗撁娴那闆r下也會(huì)要處理洼滚。例如替換HTML中的環(huán)境變量,處理CSS中的雪碧圖琳轿,甚至規(guī)避一些代碼檢查等等判沟。

最終,我們要將這些處理后的內(nèi)容發(fā)布到運(yùn)行目錄中崭篡,實(shí)現(xiàn)自動(dòng)化流程挪哄。

4. 如何用Gulp來解決這些問題(workflow)

如果細(xì)細(xì)梳理上一節(jié)所談及的各項(xiàng)目標(biāo)與工作,可以發(fā)現(xiàn)琉闪,這是一個(gè)緊密相接的工作流程(workflow)迹炼,這一節(jié)會(huì)詳細(xì)講解各個(gè)工作流程。

4.1. JS部分

首先颠毙,我們來看一下JavaScript部分的工作流程:

JavaScript部分處理流程

4.1.1. 模塊化打包

項(xiàng)目使用browserify來實(shí)現(xiàn)CommonJS斯入。如果用過browserify,應(yīng)該不會(huì)對(duì)下面這段代碼感到陌生:

browserify({
  entries: $your_entry_arr,
  cache: {},
  packageCache: {},
  plugin: $your_plugin_arr
});

通過設(shè)置一個(gè)(或一些)入口文件蛀蜜,可以將入口文件打包為一個(gè)文件刻两。但是,在多頁面應(yīng)用中滴某,最重要的有點(diǎn)就是磅摹,各個(gè)頁面會(huì)有自己的JavaScript腳本文件。在項(xiàng)目開發(fā)時(shí)霎奢,在每個(gè)HTML頁面中引入一個(gè)該頁面特有的JavaScript腳本户誓。例如頁面list.html中通過<script src="../js/list.js"></script>引入腳本。而該腳本使用CommonJS規(guī)范進(jìn)行模塊化幕侠。

// ../js/list.js
const button = require('./common/button');
// 一些button的操作
button.render('#button');
// ……

因此帝美,需要針對(duì)不同的頁面,打包出多份的JavaScript文件晤硕。首先使用node-glob獲取每個(gè)頁面的入口文件悼潭,其中SRC_JS_PATH為JavaScript源碼的路徑

/**
 * 獲取js入口文件路徑組
 * @return {Array} 文件路徑數(shù)組
 */
const getEntryJsFiles = () => (
    glob.sync(`${SRC_JS_PATH}/**/*.js`, {
        ignore: [`${SRC_JS_PATH}/*.dist.js`, `${SRC_JS_PATH}/*.mod.js`]
    })
);

然后,對(duì)每個(gè)入口文件創(chuàng)建其對(duì)應(yīng)的bundle對(duì)象并返回窗骑,以便于在每個(gè)bundle對(duì)象上進(jìn)行后續(xù)的js任務(wù)處理

let files = getEntryJsFiles();

// 遍歷所有入口文件女责,生成browserify對(duì)象
bundleTasks = files.map(ele => ({
  bundle: browserify({
  entries: [ele],
  cache: {},
  packageCache: {},
  plugin: [watchify]
  }),
  filename: ele
}));

這樣,我們就得到了一個(gè)bundleTasks创译,里面保存了所有頁面對(duì)應(yīng)的各自的入口文件的bundle對(duì)象與文件名抵知。下面我們就會(huì)對(duì)每個(gè)bundle對(duì)象都應(yīng)用上面流程圖中的工序進(jìn)行處理。我們先保留這個(gè)bundleTasks數(shù)組,來講講其他的工作流程刷喜。

4.1.2. 路徑修正

由于使用node-glob進(jìn)行匹配残制,所以匹配到的路徑不單單包含文件名,可能還會(huì)包含某些目錄名掖疮。例如初茶,可能想要匹配list.js,但是由于工作路徑等原因浊闪,實(shí)際輸出的路徑名為./page/list.js恼布。那么不進(jìn)行處理會(huì)有什么問題呢?

如果直接使用gulp.dest進(jìn)行輸出搁宾,默認(rèn)會(huì)帶上匹配出來的路徑里面的所有片段折汞,也就是說,我們可能只希望在dist目錄下生成一個(gè)list.js文件盖腿,但實(shí)際上會(huì)生成一個(gè)page目錄爽待,目錄里包含list.js文件。這就不符合我們的需求了翩腐。因此鸟款,使用gulp-rename進(jìn)行路徑調(diào)整(當(dāng)然,gulp-rename也可以重命名文件)茂卦。

在這里何什,還要推薦一個(gè)gulp工具:gulp-load-plugins。它可以自動(dòng)幫我們加載gulp-開頭的各類gulp插件等龙。因此富俄,可以很方面得進(jìn)行路徑調(diào)整。

.pipe(plugins.rename({
  dirname: ''
}))

我們需要將該步處理置于打包操作之后而咆。這里有一個(gè)需要注意的地方,由于bundle的stream是一個(gè)普通模式的stream幕袱,而gulp(vinyl)的stream則是一個(gè)objectMode的stream暴备,因此需要一些轉(zhuǎn)化與處理。這里就用到了vinyl-source-streamvinyl-buffer兩個(gè)庫:

const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const plugins = require('gulp-load-plugins')();

bundle
  .pipe(source(filename))
  .pipe(buffer())
  .pipe(plugins.rename({ // 修正路徑名稱
    dirname: ''
  }))

4.1.3. 轉(zhuǎn)碼

雖然部分瀏覽器對(duì)于es6語法已經(jīng)有了較好的原生支持们豌,但是為了能更好得保證es6代碼在瀏覽器端的正常運(yùn)行涯捻,還是推薦使用babel這樣的工具來使得生產(chǎn)環(huán)境下的代碼具有很好的瀏覽器兼容性(轉(zhuǎn)為es5)。而在gulp中只需使用gulp-babel插件就可以很方便地實(shí)現(xiàn)望迎,只需簡單幾行代碼:

.pipe(plugins.babel({
  presets: ['env']
}))

我們將該步操作置于第二步中的pipe之后障癌。

const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const plugins = require('gulp-load-plugins')();

bundle
  .pipe(source(filename))
  .pipe(buffer())
  .pipe(plugins.rename({
    dirname: ''
  }))
  .pipe(plugins.babel({ // babel
    presets: ['env']
  }))

4.1.4. 禁用代碼檢查

由于項(xiàng)目的一些特殊原因,需要將開發(fā)時(shí)的源碼和發(fā)布的生產(chǎn)環(huán)境代碼一同上傳到線上代碼庫辩尊,同時(shí)需要通過jslint的一些代碼檢查涛浙。但是發(fā)布后的代碼很多時(shí)候是不符合代碼規(guī)范的,因此,需要通過添加一些注釋來取消對(duì)部分發(fā)布后代碼的檢查轿亮。

這個(gè)插件也非常簡單疮薇,通過判斷文件類型,為文件頭部加入特定的注釋文本即可:

const through = require('through2');
const gutil = require('gulp-util');
const path = require('path');
const DIS_LINTER = {
    html: '<!-- htmlcs-disable -->',
    css: '/* csshint-disable */',
    js: '/*eslint-disable */'
};

const dislint = preText => {
  let js = new Buffer(`${DIS_LINTER['js']}\n`);
  let css = new Buffer(`${DIS_LINTER['css']}\n`);
  let html = new Buffer(`${DIS_LINTER['html']}\n`);
  let buf = {
    html,
    css,
    js
  };

  return through.obj((chunk, enc, cb) => {
    let ext = '';
    try {
      ext = path.extname(chunk.path);
    }
    catch (err) {
      console.log(err);
    }
    ext = ext.length > 0 ? ext.slice(1) : 'js';

    // gutil.log(gutil.colors.magenta('[Disable Linter]'), chunk.path);
    let preBuf = preText && preText.length > 0 ? new Buffer(preText) : buf[ext];
    if (chunk.isNull()) {
      cb(null, chunk);
    }
    if (chunk.isBuffer()) {
      chunk.contents = Buffer.concat([preBuf, chunk.contents]);
    }
    if (chunk.isStream()) {
      let stream = through();
      stream.write(preBuf);
      chunk.contents = chunk.contents.pipe(stream);
    }
    cb(null, chunk);
  });
};

module.exports = dislint;

使用該插件:

const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const plugins = require('gulp-load-plugins')();
const dislint = require('./dislint');

bundle
  .pipe(source(filename))
  .pipe(buffer())
  .pipe(plugins.rename({
    dirname: ''
  }))
  .pipe(plugins.babel({
    presets: ['env']
  }))
  .pipe(dislint()) // 取消代碼檢查

4.1.5. 添加md5戳并輸出

為了防止用戶瀏覽器緩存影響資源更新我注,可以通過添加md5戳的方式按咒,來改變文件名稱。這里用到了gulp-md5Plus這個(gè)插件但骨。

此外励七,在開發(fā)階段,我們可以通過禁用瀏覽器緩存保證獲取最新的資源奔缠,因此掠抬,在開發(fā)階段可以禁用生成md5的功能。要根據(jù)不同的環(huán)境進(jìn)行不同的操作添坊,可以使用環(huán)境變量進(jìn)行執(zhí)行剿另。gulp-util提供了這一功能。gulp-util是gulp可以看做是一個(gè)gulp的常用功能工具箱贬蛙,里面包含了log雨女、類型判斷等一系列功能。

這里阳准,我們會(huì)在非生產(chǎn)環(huán)境下氛堕,使用gutil.noop()作為一個(gè)不進(jìn)行任務(wù)處理的stream導(dǎo)出;而在生產(chǎn)環(huán)境下使用gulp-md5Plus來實(shí)現(xiàn)md5野蝇。最后讼稚,將處理后的文件輸出到指定的發(fā)布目錄:

const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const plugins = require('gulp-load-plugins')();
const dislint = require('./dislint');
const gutil = require('gulp-util');

bundle
  .pipe(source(filename))
  .pipe(buffer())
  .pipe(plugins.rename({
    dirname: ''
  }))
  .pipe(plugins.babel({
    presets: ['env']
  }))
  .pipe(dislint())
  .pipe(gutil.env.env === 'production' ? plugins.md5Plus(5, `${DIST_HTML_PATH}/**/*.html`) : gutil.noop())  // md5
  .pipe(gulp.dest(DIST_JS_PATH)) // 發(fā)布文件

4.1.6. 錯(cuò)誤處理

由于gulp是基于stream的操作,因此使用try…catch…語法顯然是無法處理拋出的異常绕沈;取而代之就需要監(jiān)聽stream上的error事件锐想。但是,在代碼里乍狐,我們總不能在每個(gè).pipe()后加上.on('error', function(){}})這樣的代碼吧赠摇,那也太臃腫了。

為了解決這個(gè)問題浅蚪,就可以使用gulp-plumber插件藕帜。只需要在stream的最前面加上它,就可以了惜傲。

bundle
  .pipe(plugins.plumber(err => {
    log(red(`[${err.plugin}]`), red(err.message));
  }))
  .pipe(source(filename))
  .pipe(buffer())
  .pipe(plugins.rename({
    dirname: ''
  }))
  .pipe(plugins.babel({
    presets: ['env']
  }))
  .pipe(dislint())
  .pipe(gutil.env.env === 'production' ? plugins.md5Plus(5, `${DIST_HTML_PATH}/**/*.html`) : gutil.noop())  // md5
  .pipe(gulp.dest(DIST_JS_PATH)) // 發(fā)布文件

4.1.7. 封裝任務(wù)

可以看到洽故,上面的一系列任務(wù)是每個(gè)入口js文件都會(huì)經(jīng)歷的,因此盗誊,我們將“路徑修正-->轉(zhuǎn)碼-->禁用代碼檢查-->md5-->輸出”這個(gè)流程封裝為一個(gè)叫做jsTask的任務(wù)时甚,并應(yīng)用在每個(gè)bundle上隘弊。

/**
 * js任務(wù)流,具體包括:
 * 模塊打包 --> 路徑修正(重命名) --> babel --> 取消代碼檢查 --> md5(production狀態(tài)) --> 產(chǎn)出
 * @param {Object} bundle 各入口文件的browserify對(duì)象
 * @param {string} filename 入口文件名
 * @return {stream} stream 對(duì)象
 */
const jsTask = ({bundle, filename}) => (
    bundle.bundle((err, buf) => {
        if (err) {
            // 瀏覽器提示
            browserSync.notify(`[Browserify Error] ${err.message}`, 10000);
            log(red('[Browserify Error]'), red(err.message));
        }
    })
    .pipe(plugins.plumber(err => {
        log(red(`[${err.plugin}]`), red(err.message));
    }))
    .pipe(source(filename))
    .pipe(buffer())
    .pipe(plugins.rename({
        dirname: ''
    }))
    .pipe(plugins.babel({
        presets: ['env']
    }))
    .pipe(dislint())
    .pipe(gutil.env.env === 'production' ? plugins.md5Plus(5, `${DIST_HTML_PATH}/**/*.html`) : gutil.noop())
    .pipe(gulp.dest(DIST_JS_PATH))
);

jsTask會(huì)包裝并返回整個(gè)js任務(wù)的流撞秋〕づ酰基于以上代碼,我們可以定義一個(gè)dist:js任務(wù)來發(fā)布js代碼:

/**
 * 獲取js入口文件路徑組
 * @return {Array} 文件路徑數(shù)組
 */
const getEntryJsFiles = () => (
  glob.sync(`${SRC_JS_PATH}/**/*.js`, {
    ignore: [`${SRC_JS_PATH}/*.dist.js`, `${SRC_JS_PATH}/*.mod.js`]
  })
);

// [發(fā)布]js代碼吻贿,其中會(huì)進(jìn)行js相關(guān)工作流程
gulp.task('dist:js', cb => {
  let files = getEntryJsFiles();

  // 遍歷所有入口文件串结,生成browserify對(duì)象
  bundleTasks = files.map(ele => ({
    bundle: browserify({
      entries: [ele],
      cache: {},
      packageCache: {},
      plugin: [watchify]
    }),
    filename: ele
  }));

  // 映射與合并js流
  let streams = bundleTasks.map(jsTask);
  return es.merge(streams);
});

// [刪除]發(fā)布目錄下的js文件
gulp.task('del:js', () => del.sync([`${DIST_JS_PATH}/*`]));

4.1.8. 自動(dòng)刷新瀏覽器

前端開發(fā)需要頻繁修改并希望能看到瀏覽器中展現(xiàn)的情況,因此舅列,解放你的F5顯然很有必要肌割。在項(xiàng)目里,可以使用browserSync來做到這一點(diǎn)帐要。

browserSync可以在代碼更新時(shí)自動(dòng)刷新瀏覽器把敞,同時(shí)還可以向?yàn)g覽器推送消息進(jìn)行展示。使用browserSync榨惠,可以建立相應(yīng)的gulp任務(wù)奋早,在第一次執(zhí)行g(shù)ulp時(shí)啟動(dòng)browserSync,并創(chuàng)建reload:browser任務(wù)赠橙,這樣在需要的時(shí)候就能方便得觸發(fā)瀏覽器刷新耽装。

const browserSync = require('browser-sync').create();

// [啟動(dòng)]browserSync
gulp.task('start:browserSync', () => browserSync.init({
  proxy: '192.168.11.23',
  notify: true
}));

// 刷新瀏覽器
gulp.task('reload:browser', cb => {
  browserSync.reload();
  cb();
});

4.1.9. 增量發(fā)布

上面介紹了對(duì)于一個(gè)js入口文件的整套工作流。然而在實(shí)際開發(fā)中期揪,我們?cè)谛薷牧四骋粋€(gè)文件之后掉奄,并不需要將所有的代碼全量再發(fā)布一遍,如果能每次只增量得發(fā)布與修改相關(guān)的js代碼凤薛,會(huì)在開發(fā)體驗(yàn)與效率上有較大的提升姓建。

同時(shí),結(jié)合browserSync可以讓你的開發(fā)效率極大獲得提升缤苫。為此速兔,我們需要watchify來進(jìn)行文件監(jiān)聽,并將其作為browserify的插件活玲,實(shí)現(xiàn)文件的增量打包編譯憨栽。通過監(jiān)聽每個(gè)bundle的update事件,可以在文件更新時(shí)重新打包并處理發(fā)布翼虫,最后到stream觸發(fā)end事件(處理完成)時(shí)刷新瀏覽器。

/**
 * 監(jiān)聽各個(gè)browserify對(duì)象的update事件
 * 在模塊更新時(shí)按需打包
 * @param {Object} bundle 各入口文件的browserify對(duì)象
 * @param {string} filename 入口文件名
 */
const addBundleTaskListeners = ({bundle, filename}) => {
  bundle.on('update', () => {
    log(blue('[Browserify Update]'), filename);
    let sm = jsTask({bundle, filename});
    // 打包完成后刷新瀏覽器(錯(cuò)誤不刷新屡萤,保留notify)
    sm.on('end', () => runSequence('reload:browser'));
  });
};

// [監(jiān)聽]為所有入口文件對(duì)應(yīng)的browserify對(duì)象添加update監(jiān)聽
gulp.task('watch:js', () => {
  bundleTasks.forEach(item => {
    addBundleTaskListeners(item);
  });
});

4.2. HTML部分

HTML部分處理流程

HTML部分主要包括兩個(gè)工作:文件內(nèi)變量的替換與文件路徑的修正珍剑。

4.2.1. 替換文件內(nèi)變量

在開發(fā)中,經(jīng)常會(huì)有類似這樣的需求:

  • 在開發(fā)環(huán)境下死陆,我們會(huì)引用一些開發(fā)機(jī)上的靜態(tài)資源招拙;然而在生產(chǎn)環(huán)境中唧瘾,則需要替換成線上的CDN地址。
  • 協(xié)同開發(fā)下别凤,不同的開發(fā)人員可能會(huì)使用不同的資源路徑饰序。
  • 為每個(gè)HTML設(shè)置title內(nèi)容,其中一部分為統(tǒng)一文字规哪,例如:我的主頁——貢獻(xiàn)列表求豫、我的主頁——個(gè)人設(shè)置…尤其在開發(fā)中,“我的主頁”可能突然要被換成“個(gè)人中心”之類的…
  • HTML中其他不常更改但可能需要各處統(tǒng)一的部分…

上面這些需求我們當(dāng)然可以通過手工替換的方式開解決诉稍,它們本身并無太多技術(shù)含量蝠嘉,但很浪費(fèi)開發(fā)人員的精力,并且還可能因?yàn)榇中拇笠猱a(chǎn)生錯(cuò)誤或遺漏杯巨。因此如果能在gulp中自動(dòng)替換這些變量蚤告,必然會(huì)節(jié)省很多麻煩。

參考一些其他工具或腳手架里的功能服爷,我們的目標(biāo)效果是:在項(xiàng)目根目錄下定義這些變量(例如在.env或.env.local文件中)

$ONE_CDN$=http://cp01-test.XXXX.com
$ONE_TITLE$=我的主頁

然后在HTML中直接使用

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>$ONE_TITLE$——貢獻(xiàn)列表</title>
    <link rel="shortcut icon" href="/img/icon.png" />

    <script src="$ONE_CDN$/vendors/bower_components/jquery/dist/jquery.min.js"></script>
    <script src="$ONE_CDN$/vendors/bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
    <script src="$ONE_CDN$/js/app.min.js"></script>
    <link href="$ONE_CDN$/vendors/bower_components/animate.css/animate.min.css" rel="stylesheet">
    <link href="$ONE_CDN$/vendors/bower_components/bootstrap/dist/js/bootstrap.min.css" rel="stylesheet">
    ……
</head>

下面杜恰,就要需要一個(gè)方法能夠讀取出.env和.env.local文件中的所有變量(默認(rèn)先使用.env.local,可以理解為.env.local會(huì)覆蓋.env中的同名變量)

/**
 * trim
 * @param {string} str 待處理字符串
 * @return {string} 處理后的字符串
 */
const trim = str => str.replace(/(^\s*)|(\s*$)/g, '');

/**
 * 獲取文件中的環(huán)境變量
 * @return {Object} 環(huán)境變量map
 */
const getEnv = () => {
    let prepare = ['.env.local', '.env'];
    let env = {};

    /**
     * 檢查.env中變量名是否合法
     * 全部使用大寫字母仍源,用_連接心褐,第一個(gè)單詞為ONE,首尾使用$
     * @param {string} key 待檢查的變量名
     * @return {boolean} 檢查結(jié)果镜会,合法true檬寂,非法false
     */
    function envCheck(key) {
        return /^\$ONE(_[A-Z]+)+\$$/.test(key);
    }

    /**
     * 讀取文件中的變量
     * @param {string} filename 配置文件
     * @return {Object} 配置變量
     */
    function readEnvFile(filename) {
        let env = {};
        try {
            let filepath = path.resolve('.', filename);
            if (fs.existsSync(filepath) && fs.statSync(filepath).isFile()) {
                fs.readFileSync(filepath, 'utf-8')
                    .split(/\r?\n/)
                    .filter(ele => ele !== '')
                    .forEach(ele => {
                        let pairs = ele.split('=');
                        let key = trim(pairs[0]);
                        let value = trim(pairs[1]);
                        if (envCheck(key)) {
                            env[key] = value;
                        }
                        else {
                            log(red('[env]'), `無效的變量名: ${key}`, 'tip: 全部使用大寫字母,用_連接戳表,第一個(gè)單詞為ONE桶至,首尾使用$');
                        }
                    });
            }
        }
        catch (err) {
            log(red('[env]'), '讀取env變量出錯(cuò)', err);
        }
        finally {
            return env;
        }
    }

    let envArr = [{}];
    // 優(yōu)先尋找本地配置.env.local
    while (prepare.length) {
        // 生產(chǎn)環(huán)境下優(yōu)先使用.env
        let filename = gutil.env.env === 'production' ? prepare.shift() : prepare.pop();
        envArr.push(readEnvFile(filename));
    }

    return Object.assign.apply(null, envArr);
};

getEnv()方法可以讀取所有定義的環(huán)境變量,并保存為一個(gè)鍵值對(duì)形式的對(duì)象匾旭,鍵名是變量名镣屹,值則是變量的值

// getEnv()
{
  'ONE_CDN': 'http://cp01-test.XXXX.com',
  'ONE_TITLE': '我的主頁'
}

由于要替換文件內(nèi)容,我們可以使用event-stream庫來進(jìn)行stream的操作价涝。使用其中的.replace()方法來替換文件內(nèi)容女蜈,代碼片段如下:

const es = require('event-stream');

let pipe = fs.createReadStream(sourceFile);

// 添加管道,替換.env中的環(huán)境變量
for (let k in envMap) {
  pipe = pipe.pipe(es.replace(k, envMap[k]));
}

4.4.2. 修正文件路徑

類似JavaScript中的文件路徑修正操作色瘩,在HTML中同樣使用gulp-rename來實(shí)現(xiàn)伪窖,路徑修正部分代碼片段如下:

// 路徑格式化正則
let reg = new RegExp(`^${path.relative('.', SRC_HTML_PATH)}`);

return (
  pipe.pipe(source(sourceFile))
    .pipe(dislint())
    .pipe(plugins.rename(p => {
      // 格式化目標(biāo)路徑
      p.dirname = p.dirname.replace(reg, '');
    }))
    .pipe(gulp.dest(DIST_HTML_PATH))
);

4.2.3. 封裝任務(wù)

將變量替換與文件路徑修正兩部分代碼封裝為一個(gè)任務(wù)函數(shù)htmlTask

/**
 * html發(fā)布任務(wù)
 * @param {string} sourceFile 需要發(fā)布的目標(biāo)html文件
 * @return {stream} 文件流
 */
const htmlTask = sourceFile => {
  let pipe = fs.createReadStream(sourceFile);

  // 添加管道,替換.env中的環(huán)境變量
  for (let k in envMap) {
    pipe = pipe.pipe(es.replace(k, envMap[k]));
  }

  // 路徑格式化正則
  let reg = new RegExp(`^${path.relative('.', SRC_HTML_PATH)}`);

  return (
    pipe.pipe(source(sourceFile))
      .pipe(dislint())
      .pipe(plugins.rename(p => {
        // 格式化目標(biāo)路徑
        p.dirname = p.dirname.replace(reg, '');
      }))
      .pipe(gulp.dest(DIST_HTML_PATH))
  );
};

在此基礎(chǔ)上居兆,創(chuàng)建一個(gè)gulp任務(wù)用于HTML文件的發(fā)布

// [發(fā)布]html頁面
gulp.task('dist:html', () => {
  let files = glob.sync(`${SRC_HTML_PATH}/**/*.html`);
  let streams = files.map(htmlTask);
  return es.merge(streams);
});

同時(shí)覆山,還需要把已有的文件刪除,這里用到了del這個(gè)包

const del = require('del');

// [刪除]發(fā)布目錄額下的html文件
gulp.task('del:html', () => del.sync([`${DIST_HTML_PATH}/*`]));

4.3. CSS部分

CSS部分處理流程

CSS部分的處理流程中大部分與之前的操作大同小異泥栖,其中最主要的區(qū)別是在CSS中使用到gulp-less插件與gulp-minify-css插件分別對(duì)less文件進(jìn)行預(yù)處理與壓縮

gulp.src(`${SRC_CSS_PATH}/**/*.less`) //多個(gè)文件以數(shù)組形式傳入
  .pipe(less())
  .pipe(minifyCss())
  .pipe(gulp.dest('dist/css')); 

最終CSS部分的處理任務(wù)如下:

// [刪除]發(fā)布目錄下的css文件
gulp.task('del:css', () => del.sync([`${DIST_CSS_PATH}/*`]));

// [發(fā)布]css文件
gulp.task('dist:css', function () {
  gulp.src(`${SRC_CSS_PATH}/**/*.less`) 
    .pipe(plugins.rename({
        dirname: ''
    }))
    .pipe(less())
    .pipe(minifyCss())
    .pipe(gulp.dest('dist/css')); 
});

4.4. 組合與管理這些任務(wù)

我們定義上面一系列的任務(wù)簇宽,但最終的目標(biāo)是將這些任務(wù)組合起來勋篓,讓他們變成一條指令(或某幾條指令)。為了更好得組織任務(wù)依賴魏割,控制任務(wù)流程譬嚣,我們使用run-sequence來控制這些任務(wù)的執(zhí)行。

最常見的钞它,首先是在開發(fā)時(shí)拜银,希望輸入gulp就可以進(jìn)入開發(fā)模式,能夠監(jiān)聽變化并自動(dòng)刷新瀏覽器须揣。

// [監(jiān)聽]為所有入口文件對(duì)應(yīng)的browserify對(duì)象添加update監(jiān)聽
gulp.task('watch:js', () => {
  bundleTasks.forEach(item => {
    addBundleTaskListeners(item);
  });
});

// [監(jiān)聽]更新CSS
gulp.task('watch:css', () => {
  gulp.watch(['!**/gulpfile.js', 'src/**/*.css'], () => {
    runSequence(
      'del:css',
      'dist:css',
      'reload:browser'
    );
  });
  cb();
});

// [啟動(dòng)]browserSync
gulp.task('start:browserSync', () => browserSync.init({
  proxy: '192.168.11.23',
  notify: true
}));

// 刷新瀏覽器
gulp.task('reload:browser', cb => {
  browserSync.reload();
  cb();
});

// [監(jiān)聽]html文件變化
gulp.task('watch:html', cb => {
  // 監(jiān)聽html變化盐股,全量發(fā)布新html
  gulp.watch(['!**/gulpfile.js', 'src/**/*.html'], () => {
    runSequence(
      'del:html',
       'dist:html',
      'reload:browser'
    );
  });
  cb();
});

// 發(fā)布模式
// build任務(wù),進(jìn)行項(xiàng)目資源發(fā)布
gulp.task('build', cb => {
  runSequence(
    ['del:html', 'del:js'],
    'dist:html',
    'dist:js',
    cb
  );
});

// 開發(fā)模式
// 執(zhí)行發(fā)布耻卡,并進(jìn)行監(jiān)聽
gulp.task('default', cb => {
  runSequence(
    'build',
    'start:browserSync',
    ['watch:js', 'watch:html', 'watch:css'],
    cb
  );
});

當(dāng)然疯汁,常用的還有發(fā)布任務(wù)

// 發(fā)布模式
// 構(gòu)建資源發(fā)布,并退出gulp進(jìn)程
gulp.task('dist', cb => {
  gutil.env.env = gutil.env.env === undefined ? 'production' : gutil.env.env;
  runSequence(
    'build',
    () => {
      cb();
      process.nextTick(process.exit);
    }
  );
});

總結(jié)

文章里主要整理了我在項(xiàng)目中用到的一些解決方案與實(shí)踐方法卵酪。其中也還存在一些不足幌蚊,例如HTML與CSS的增量發(fā)布等,這些都是之后可以再進(jìn)行優(yōu)化的地方溃卡。

完溢豆。


Happy Coding!


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瘸羡,一起剝皮案震驚了整個(gè)濱河市漩仙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌犹赖,老刑警劉巖队他,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異峻村,居然都是意外死亡麸折,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門粘昨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來垢啼,“玉大人,你說我怎么就攤上這事张肾“盼觯” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵吞瞪,是天一觀的道長放刨。 經(jīng)常有香客問我,道長尸饺,這世上最難降的妖魔是什么进统? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮浪听,結(jié)果婚禮上螟碎,老公的妹妹穿的比我還像新娘。我一直安慰自己迹栓,他們只是感情好掉分,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著克伊,像睡著了一般酥郭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上愿吹,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天不从,我揣著相機(jī)與錄音,去河邊找鬼犁跪。 笑死椿息,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的坷衍。 我是一名探鬼主播寝优,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼枫耳!你這毒婦竟也來了乏矾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤迁杨,失蹤者是張志新(化名)和其女友劉穎钻心,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仑最,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡扔役,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了警医。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片亿胸。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖预皇,靈堂內(nèi)的尸體忽然破棺而出侈玄,到底是詐尸還是另有隱情,我是刑警寧澤吟温,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布序仙,位于F島的核電站,受9級(jí)特大地震影響鲁豪,放射性物質(zhì)發(fā)生泄漏潘悼。R本人自食惡果不足惜律秃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望治唤。 院中可真熱鬧棒动,春花似錦、人聲如沸宾添。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缕陕。三九已至粱锐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扛邑,已是汗流浹背怜浅。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鹿榜,地道東北人海雪。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像舱殿,于是被迫代替她去往敵國和親奥裸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容

  • gulpjs是一個(gè)前端構(gòu)建工具沪袭,與gruntjs相比湾宙,gulpjs無需寫一大堆繁雜的配置參數(shù),API也非常簡單冈绊,學(xué)...
    依依玖玥閱讀 3,157評(píng)論 7 55
  • gulpjs是一個(gè)前端構(gòu)建工具侠鳄,與gruntjs相比,gulpjs無需寫一大堆繁雜的配置參數(shù)死宣,API也非常簡單伟恶,學(xué)...
    井皮皮閱讀 1,305評(píng)論 0 10
  • gulpjs是一個(gè)前端構(gòu)建工具,與gruntjs相比毅该,gulpjs無需寫一大堆繁雜的配置參數(shù)博秫,API也非常簡單,學(xué)...
    小裁縫sun閱讀 932評(píng)論 0 3
  • 在現(xiàn)在的前端開發(fā)中眶掌,前后端分離挡育、模塊化開發(fā)、版本控制朴爬、文件合并與壓縮即寒、mock數(shù)據(jù)等等一些原本后端的思想開始...
    Charlot閱讀 5,449評(píng)論 1 32
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器母赵,智...
    卡卡羅2017閱讀 134,708評(píng)論 18 139