放棄webpack,擁抱gulp

別被標(biāo)題嚇到,哈哈,即使現(xiàn)在vite橫空出世,社區(qū)光芒四射,兩個(gè)字很快,但是webpack依舊寶刀未老,依然扛起前端工程化的大梁,但是今天我為啥說(shuō)要擁抱gulp,因?yàn)槲覀兂33砸坏啦?,所以要換個(gè)口味,這樣才營(yíng)養(yǎng)均衡。

gulp定義是:用自動(dòng)化構(gòu)建工具增強(qiáng)你的工作流程,是一種基于任務(wù)文件流方式,你可以在前端寫(xiě)一些自動(dòng)化腳本,或者升級(jí)歷史傳統(tǒng)項(xiàng)目,解放你重復(fù)打包,壓縮,解壓之類的操作。

個(gè)人理解gulp是一種命令式編程的體驗(yàn),更注重構(gòu)建過(guò)程,所有的任務(wù)需要你自己手動(dòng)創(chuàng)建,你會(huì)對(duì)構(gòu)建流程會(huì)非常清楚,這點(diǎn)不像webpack,webpack就是一個(gè)開(kāi)箱即用的聲明式方式,webpack是一個(gè)模塊化打包工具,內(nèi)部細(xì)節(jié)隱藏非常之深,你也不需關(guān)注細(xì)節(jié),你只需要照著提供的API以及引入對(duì)應(yīng)的loader和plugin使用就行。

言歸正傳,為了飲食均衡,今天一起學(xué)習(xí)下gulp

正文開(kāi)始...

搭建一個(gè)簡(jiǎn)單的前端應(yīng)用
相比較webpack,其實(shí)gulp的項(xiàng)目結(jié)構(gòu)更偏向傳統(tǒng)的應(yīng)用,只是我們借助gulp工具解放我們的一些代碼壓縮、es6編譯、打包以及在傳統(tǒng)項(xiàng)目中都可以使用less體驗(yàn)。

在gulp目錄下新建01-simple-demo

根目錄下生成默認(rèn)package.json

npm init -y
然后在public目錄下新建images、css、js、index.html

文件結(jié)構(gòu),大概就這樣


然后在安裝gulp

npm i gulp --save-dev
在根目錄下新建gulpfile.js我們先在gulpfile.js中寫(xiě)入一點(diǎn)內(nèi)容,測(cè)試一下

const defaultTask = (cb) => {
  console.log('hello gulp')
  cb();
}
exports.default = defaultTask;
然后我們?cè)诿钚袌?zhí)行

npx gulp

當(dāng)我們執(zhí)行npx gulp時(shí)會(huì)默認(rèn)運(yùn)行g(shù)ulpfile.js導(dǎo)出的default,在gulpfile.js導(dǎo)出的任務(wù)會(huì)?注冊(cè)到gulp任務(wù)中

在gulp中任務(wù)主要分兩種,一種是公開(kāi)任務(wù)、另一種是私有任務(wù)

公開(kāi)任務(wù)可以直接在命令執(zhí)行npx gulp xxx調(diào)用執(zhí)行,比如下面的defaultTask就是一個(gè)公開(kāi)任務(wù),只要被導(dǎo)出就是一個(gè)公開(kāi)任務(wù),沒(méi)有被導(dǎo)出就是一個(gè)私有任務(wù)。

...
exports.default = defaultTask;
公有任務(wù)taskJS

// gulpfile.js
const { src, dest } = require('gulp');
const pathDir = (dir) => {
  return path.resolve(__dirname, dir);
}
// todo 執(zhí)行ts任務(wù),將js目錄下的js打包到dist/js目錄下
const taskJS = () => {
  return src(pathDir('public/**/*.js'), { sourcemaps: true }).pipe(dest(pathDir('dist/js')))
}
exports.taskJS = taskJS;
然后你在命令行執(zhí)行

npx gulp taskJS


至此你會(huì)發(fā)現(xiàn)dist目錄下就有生成的js了
安裝less
npm i less gulp-less --save-dev
在css/index.less中寫(xiě)入測(cè)試css的代碼

@bgcolor: yellow;
@defaultsize: 20px;
body {
  background-color: @bgcolor;
}
h1 {
  font-size: @defaultsize;
}
在gulpfile.js中寫(xiě)入編譯less的任務(wù),需要gulp-less

const { src, dest } = require('gulp');
const less = require('gulp-less');
const pathDir = (dir) => {
  return path.resolve(__dirname, dir);
}
...
// todo less任務(wù)
const taskLess = () => {
  // css目錄洗的所有.less文件,dest輸出到dist/css目錄下
  return src(pathDir('public/css/*.less')).pipe(less()).pipe(dest(pathDir('dist/css')))
}
exports.taskLess = taskLess;
命令行運(yùn)行npx gulp taskLess,結(jié)果如下


圖片資源
使用一個(gè)gulp-image插件對(duì)圖片進(jìn)行無(wú)損壓縮處理

// gulpfile.js
const { src, dest } = require('gulp');
const image = require('gulp-image');
const path = require('path');
const pathDir = (dir) => {
  return path.resolve(__dirname, dir);
}
...
// todo 圖片資源
const taskImage = () => {
  return src(pathDir('public/images/*.*')).pipe(image()).pipe(dest(pathDir('dist/images')))
}
exports.taskImage = taskImage;

一頓操作發(fā)現(xiàn),最新版本不支持esm,所以還是降低版本版本,這里降低到6.2.1版本,這里只能使用ejs方式

然后運(yùn)行npx gulp taskImage


圖片壓縮得不小
在這之前,我們分別定義了三個(gè)不同的任務(wù),gulp導(dǎo)出的任務(wù)有公開(kāi)任務(wù)和私有任務(wù),多個(gè)公開(kāi)任務(wù)可以串行組合使用

組合任務(wù) series
因此我可以將之前的介個(gè)任務(wù)組合在一起

// gulpfile.js
const { src, dest, series } = require('gulp');
const less = require('gulp-less');
const image = require('gulp-image');
const path = require('path');
const pathDir = (dir) => {
  return path.resolve(__dirname, dir);
}
// todo js任務(wù)
const taskJS = () => {
  return src(pathDir('public/**/*.js'), { sourcemaps: true }).pipe(dest(pathDir('dist/js')))
}
...
// series組合多個(gè)任務(wù)
const seriseTask = series(taskJS, taskLess, taskLess, taskImage)
exports.seriseTask = seriseTask;
當(dāng)我在命令行npx gulp seriseTask時(shí)


已經(jīng)在dist生成對(duì)應(yīng)的文件了
編譯轉(zhuǎn)換es6
在我們index.js,很多時(shí)候是寫(xiě)的es6,在gulp中我們需要一些借助一些插件gulp-babel,另外我們需要安裝另外兩個(gè)babel核心插件@babel/core,@babel/preset-env

 npm i gulp-babel @babel/core @babel/preset-env
在gulpfile.js中我們需要修改下

...
const babel = require('gulp-babel');
// todo js任務(wù)
// 用babel轉(zhuǎn)換es6語(yǔ)法糖
const taskJS = () => {
  return src(pathDir('public/**/*.js'), { sourcemaps: true }).pipe(babel({
    presets: ['@babel/preset-env']
  })).pipe(dest(pathDir('dist/js')))
}
當(dāng)我們?cè)趈s/index.js寫(xiě)入一段測(cè)試代碼

js/index.js
const appDom = document.getElementById('app');
appDom.innerHTML = 'hello gulp';
const fn = () => {
  console.log('公眾號(hào):Web技術(shù)學(xué)苑,好好學(xué)習(xí),天天向上')
}
fn();
運(yùn)行npx gulp seriseTask


箭頭函數(shù)和const申明的變量就變成了es5了
通常情況下,一般打包后的dist下的css或者js都會(huì)被壓縮,在gulp中也是需要借助插件來(lái)完成

壓縮js與css
壓縮js

...
const teser = require('gulp-terser');
// todo js任務(wù)
const taskJS = () => {
  return src(pathDir('public/**/*.js'), { sourcemaps: true }).pipe(babel({
    presets: ['@babel/preset-env']
  })).pipe(teser({
    mangle: {
      toplevel: true // 混淆代碼
    }
  })).pipe(dest(pathDir('dist/js')))
}
...
壓縮css

...
const uglifycss = require('gulp-uglifycss');
// todo less任務(wù)
const taskLess = () => {
  return src(pathDir('public/css/*.less')).pipe(less()).pipe(uglifycss()).pipe(dest(pathDir('dist/css')))
}
...
在這之前我們?cè)谳敵鰀est時(shí)候我們都指向了一個(gè)具體的文件目錄,在src這個(gè)api中是創(chuàng)建流,從文件中讀取vunyl對(duì)象,本身也提供了一個(gè)base屬性,因此你可以像下面這樣寫(xiě)

const { src, dest, series } = require('gulp');
const less = require('gulp-less');
const image = require('gulp-image');
const babel = require('gulp-babel');
const teser = require('gulp-terser');
const uglifycss = require('gulp-uglifycss');
const path = require('path');
const pathDir = (dir) => {
  return path.resolve(__dirname, dir);
}
// 設(shè)置base,當(dāng)輸出文件目標(biāo)dist文件時(shí),會(huì)自動(dòng)拷貝當(dāng)前文件夾到目標(biāo)目錄
const basePath = {
  base: './public'
};
// todo js任務(wù)
const taskJS = () => {
  return src(pathDir('public/**/*.js', basePath)).pipe(babel({
    presets: ['@babel/preset-env']
  })).pipe(teser({
    mangle: {
      toplevel: true // 混淆代碼
    }
  })).pipe(dest(pathDir('dist')))
}
// todo less任務(wù)
const taskLess = () => {
  return src(pathDir('public/css/*.less'), basePath).pipe(less()).pipe(uglifycss()).pipe(dest(pathDir('dist')))
}
// todo 圖片資源,有壓縮,并輸出到對(duì)應(yīng)的dist/images文件夾下
const taskImage = () => {
  return src(pathDir('public/images/*.*'), basePath).pipe(image()).pipe(dest(pathDir('dist')))
}
// todo html
const taskHtml = () => {
  return src(pathDir('public/index.html'), basePath).pipe(dest(pathDir('dist')))
}
const defaultTask = (cb) => {
  console.log('hello gulp')
  cb();
}

// series組合多個(gè)任務(wù)
const seriseTask = series(taskHtml, taskJS, taskLess, taskLess, taskImage)

exports.default = defaultTask;
exports.taskJS = taskJS;
exports.taskLess = taskLess;
exports.taskImage = taskImage;
exports.seriseTask = seriseTask;
將資源注入html中
在gulp中,任務(wù)之間的依賴關(guān)系需要我們自己手動(dòng)寫(xiě)一些執(zhí)行任務(wù)流,現(xiàn)在一些打包后的dist的文件并不會(huì)自動(dòng)注入html中。

參考gulp-inject[1]

...
const inject = require('gulp-inject');
...
// 將css,js插入html中
const injectHtml = () => {
  // 目標(biāo)資源
  const targetSources = src(['./dist/**/*.js', './dist/**/*.css'], { read: false });
  // 目標(biāo)html
  const targetHtml = src('./dist/*.html')
  // 把目標(biāo)資源插入目標(biāo)html中,同時(shí)輸出到dist文件下
  const result = targetHtml.pipe(inject(targetSources)).pipe(dest('dist'));
  return result
}
// series串行組合多個(gè)任務(wù)
const seriseTask = series(taskHtml, taskJS, taskLess, taskLess, taskImage, injectHtml)

exports.seriseTask = seriseTask;
注意一個(gè)執(zhí)行順序,必須是等前面任務(wù)執(zhí)行完了,再注入,所以在series任務(wù)的最后才執(zhí)行injectHtml操作

并且在public/index.html下,還需要加入一段注釋

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>gulp</title>
    <!-- inject:css -->
    <!-- endinject -->
  </head>
  <body>
    <div id="app"></div>
    <!-- inject:js -->
    <!-- endinject -->
  </body>
</html>
當(dāng)我們運(yùn)行npx gulp seriseTask時(shí)


創(chuàng)建本地服務(wù)
我們需要將前面所有的js,css,html組織起來(lái),在本地服務(wù)中使用






參考browser-sync[2]

const { src, dest, series, watch } = require('gulp');
const browserSync = require('browser-sync');
...

const taskBuild = seriseTask;
// 本地服務(wù)
const taskDevServer = () => {
  // 監(jiān)聽(tīng)public所有目錄下,只要文件發(fā)生改變,就重新加載
  watch(pathDir('public'), taskBuild);
  // 創(chuàng)建服務(wù)
  const server = browserSync.create();
  // 調(diào)用init開(kāi)啟端口訪問(wèn)
  server.init({
    port: '8081', //設(shè)置端口
    open: true,  // 自動(dòng)打開(kāi)瀏覽器
    files: './dist/*', // dist文件
    server: {
      baseDir: './dist'
    }
  })
}
exports.taskDevServer = taskDevServer;
當(dāng)我們運(yùn)行npx gulp taskDevServer時(shí),瀏覽器會(huì)默認(rèn)打開(kāi)http://localhost:8081


我們使用了一個(gè)watch監(jiān)聽(tīng)public目錄下的所有文件,如果文件有變化時(shí),會(huì)執(zhí)行taskBuild任務(wù)會(huì)在dist目錄下生成對(duì)應(yīng)的文件,然后會(huì)啟動(dòng)一個(gè)本地服務(wù),打開(kāi)一個(gè)8081的端口就可以訪問(wèn)應(yīng)用了。

至此一個(gè)一個(gè)用gulp搭建的前端應(yīng)用終于可以了。

重新組織gulpfile
最后我們可以再重新組織一下gulpfile.js,因?yàn)槎鄠€(gè)任務(wù)寫(xiě)在一個(gè)文件里貌似不太那么好維護(hù),隨著業(yè)務(wù)迭代,會(huì)越來(lái)越多,因此,有必要將任務(wù)分解一下

在根目錄新建task,我們把所有的任務(wù)如下

common.js

// task/common.js
const path = require('path');
const pathDir = (dir) => {
  return path.join(__dirname, '../', dir);
}
const rootDir = path.resolve(__dirname, '../');
const basePath = {
  base: './public'
};
const targetDest = 'dist';
module.exports = {
  rootDir,
  pathDir,
  basePath,
  targetDest
};
injectHtml.js

// task/injectHtml.js
const { src, dest } = require('gulp');
const inject = require('gulp-inject');
const { targetDest, rootDir } = require('./common.js');
// 將css,js插入html中
const injectHtml = () => {
  // 目標(biāo)資源
  const targetSources = src([`${rootDir}/${targetDest}/**/*.js`, `${rootDir}/${targetDest}/**/*.css`]);
  // 目標(biāo)html
  const targetHtml = src(`${rootDir}/${targetDest}/*.html`)
  // 把目標(biāo)資源插入目標(biāo)html中,同時(shí)輸出到dist文件下
  const result = targetHtml.pipe(inject(targetSources, { relative: true })).pipe(dest(targetDest));
  return result
}
module.exports = injectHtml;
taskDevServer.js

const { watch } = require('gulp');
const path = require('path');
const browserSync = require('browser-sync');
const { pathDir, targetDest, rootDir } = require('./common.js');
const taskDevServer = (taskBuild) => {
  return (options = {}) => {
    const defaultOption = {
      port: '8081', //設(shè)置端口
      open: true,  // 自動(dòng)打開(kāi)瀏覽器
      files: `${rootDir}/${targetDest}/*`, // 當(dāng)dist文件下有改動(dòng)時(shí),會(huì)自動(dòng)刷新頁(yè)面
      server: {
        baseDir: `${rootDir}/${targetDest}` // 基于當(dāng)前dist目錄
      },
      ...options
    }
    // 監(jiān)聽(tīng)public所有目錄下,只要文件發(fā)生改變,就重新加載
    watch(pathDir('public'), taskBuild);
    const server = browserSync.create();
    server.init(defaultOption);
  }
}
module.exports = taskDevServer;
...

task/index.js

const injectHtml = require('./injectHtml.js');
const taskDevServer = require('./taskDevServer.js');
const taskHtml = require('./taskHtml.js');
const taskImage = require('./taskImage.js');
const taskJS = require('./taskJS.js');
const taskLess = require('./taskLess.js');
module.exports = {
  injectHtml,
  taskDevServer,
  taskHtml,
  taskImage,
  taskJS,
  taskLess
}
在gulpfile.js中,我們修改下

// gulpfile.js
const { series } = require('gulp');
const { injectHtml, taskDevServer, taskHtml, taskImage, taskJS, taskLess } = require('./task/index.js')

// series組合多個(gè)任務(wù)
const seriseTask = series(taskHtml, taskJS, taskLess, taskLess, taskImage, injectHtml);
// 本地服務(wù)
const devServer = taskDevServer(seriseTask);
// 啟動(dòng)服務(wù)
const server = () => {
  devServer({
    port: 9000
  });
}
const taskBuild = seriseTask;
const defaultTask = (cb) => {
  console.log('hello gulp')
  cb();
}
exports.default = defaultTask;
exports.server = server;
exports.build = taskBuild;
我們?cè)趐ackage.json中新增命令

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "server": "gulp server",
    "build": "gulp build"
  },
npm run build

在啟動(dòng)server之前,我們先執(zhí)行npm run build,然后再執(zhí)行下面命令,保證browserSync創(chuàng)建的服務(wù)文件夾存在,不然頁(yè)面打開(kāi)就404錯(cuò)誤

npm run server

至此gulp搭建一個(gè)簡(jiǎn)單的應(yīng)該就已經(jīng)完全ok了

這頁(yè)面背景貌似有點(diǎn)黃

總結(jié)
gulpjs開(kāi)發(fā)是一個(gè)任務(wù)流的開(kāi)發(fā)方式,它的核心思想就是用自動(dòng)化構(gòu)建工具增強(qiáng)你的工作流,所有的自動(dòng)化工作流操作都牢牢的掌握在自己手上,你可以用gulp寫(xiě)一些自動(dòng)化腳本,比如,文件上傳,打包,壓縮,或者改造傳統(tǒng)的前端應(yīng)用。

用gulp寫(xiě)了一個(gè)簡(jiǎn)單的應(yīng)用,但是發(fā)現(xiàn)中途需要找好多gulp插件,gulp的生態(tài)還算可以,3w多個(gè)star,生態(tài)相對(duì)豐富,但是有些插件常年不更新,或者版本更新不支持,比如gulp-image,當(dāng)你按照官方文檔使用最新的包時(shí),不支持esm,你必須降低版本6.2.1,改用cjs才行

使用gulp的一些常用的api,比如src、dest、series,以及browser-sync實(shí)現(xiàn)本地服務(wù),更多api[3]參考官方文檔。

即使項(xiàng)目時(shí)間再多,也不要用gulp搭建前端應(yīng)用,因?yàn)閣ebpack生態(tài)很強(qiáng)大了,看gulp的最近更新還是2年前,但是寫(xiě)個(gè)自動(dòng)化腳本,還算可以,畢竟gulp的理念就是用自動(dòng)化構(gòu)建工具增強(qiáng)你工作流程,也許當(dāng)你接盤(pán)傳統(tǒng)項(xiàng)目時(shí),一些打包,拷貝,壓縮文件之類的,可以嘗試用用這個(gè)。

本文示例code-example[4]

參考資料
[1]
gulp-inject: https://www.npmjs.com/package/gulp-inject

[2]
browser-sync: https://browsersync.io/docs/gulp

[3]
api: https://www.gulpjs.com.cn/docs/api/src/

[4]
code-example: https://github.com/maicFir/lessonNote/tree/master/gulp/01-simple-demo

最后,看完覺(jué)得有收獲的,點(diǎn)個(gè)贊,在看,轉(zhuǎn)發(fā),收藏等于學(xué)會(huì),歡迎關(guān)注Web技術(shù)學(xué)苑,好好學(xué)習(xí),天天向上!





作者:Maic

歡迎關(guān)注微信公眾號(hào) :web技術(shù)學(xué)苑