帶你揭開自動化構建的神祕面紗


前言:對於我們這些日常基於腳手架項目開發,使用 yarn/npm run start、yarn/npm run build 等命令完成自動化構建的開發者來說,重要的自動化構建彷彿變成了前端的一個黑盒知識。但是,掌握前端工程的自動化構建,是學習前端工程化以及進階高級前端所必不可缺少的部分。

通過這篇文章,我嘗試把我自己對自動化構建知識體系的系統化認識托盤而出,希望能夠對閣下有所幫助。同樣,我更喜歡的是您能批判我的一些觀點或者指出我的一些問題,因爲忠言逆耳利於行。

注意:文章的側重點是對自動化構建知識的系統化探討,不會是對某一個具體工具使用上的面面俱到,畢竟那是官方文檔該做的事情。

對於自動化構建知識體系的系統化認識,從個人認識出發,我用腦圖做了以下整理:

接下來的行文,我都會圍繞這副腦圖展開,如果您有興趣繼續往下看下去,我希望您能在這幅圖上停留多一些時間。

好地,按照上述腦圖中的邏輯,接下來我會分成以下幾個部分來展開探討本文。

好的,理清楚行文思路之後,進入第一點,理解前端工程的自動化構建。

一:理解前端工程的自動化構建

1. 先理解一下爲什麼會出現這玩意

簡單來說,隨着前端需求和項目的日益複雜,出於提高開發效率、用戶體驗以及其它工程上的需要,我們通常會藉助很多更高階的語法(如 es6、ts、less 等)或者服務(如 web server)等來幫助我們更快更好的開發、調試、增強一個前端工程。但是,這會導致一個問題,就是我們寫的代碼會離瀏覽器或 node 可解析運行的代碼越來越遠。

爲了解決這個問題,前端工程構建的概念就逐漸豐富完整了起來。也就是說,前端工程構建就是指前端項目從源代碼到一個能按需運行(開發環境、生產環境)的前端工程所需要做的所有事情。由於前端工程的構建過程中會包含很多任務並且工程需要頻繁構建,所以按照任何簡單機械的重複勞動都應該讓機器去完成的思想,我們應該自動化去完成工程的構建,提高構建效率

2. 從這玩意的具體實踐角度來再理解一次

前面提到了前端工程自動化構建的本質,但是這個理解離我們具體實踐它還是有點遠,下面再說說我對自動化構建實踐上的理解吧。

我認爲,前端工程構建的具體實踐形式就是一個任務流,完成了這個任務流中的所有任務即完成了前端工程構建。而自動化構建,也就是不用手動的執行這個任務流中的一個個任務。

好的,經過上面兩點講解,我覺得我已經把我對它的所有理解都已經傾囊相授了。

爲了引導接下來對自動化構建具體實現的講解,下面我們再從自動化構建就是完成一個任務流這個實踐角度理解來展開細緻探討,也就是以下這兩點,即:

3. 理解構建任務流中的任務

對比於 JavaScript 的函數,個人對任務是這麼分類的:

同步任務和異步任務無須解釋,這裏說說並行任務和串行任務。任務並行可以用於縮短多個任務的執行時間。因爲 node 是單線程執行的,我認爲它並不能縮短多個同步任務並行的執行時間,但是構建過程中的任務通常都會涉及到 IO 流,所以大部分任務都是異步任務,IO 任務並行可以避免 IO 阻塞線程執行,進而縮短多個任務的執行時間。

而任務串行可以保證任務之間的有序執行,比如在開發環境下,我們肯定要先執行編譯任務,然後才能執行啓動開發服務器的任務。

理解了構建過程中的任務之後,下面再列舉一些日常開發的構建過程中,我們所常見到的任務。

前端構建過程中的常見任務

f55NXH

除了上述表格中列舉的任務之外,在不同的項目不同的場景中還會有不同的構建任務,這裏就不再一一贅述了。上面說到構建其實就是完成一個任務流,在理解和認識了常見任務之後,接下來我們理解一下前端工程當中的任務流。

4. 理解構建任務流

任務流的理解可不只是多個任務這麼簡單,任務流是要爲目的服務的,就好比生產一個產品,必須完整跑完生產流水線一樣。所以我們這裏得從構建目的的角度來理解任務流。

前端構建是爲前端工程服務的,而前端工程又是爲用戶服務的。對應於開發環境和生產環境,前端工程可以分爲開發環境工程和生產環境工程,其中開發環境工程爲開發者服務,生產環境工程爲用戶服務。

滿足工程使用者的需求是我們構建工程的終極目的,所以有必要投其所好,根據工程的使用者不同,完成他所需要的的一連串任務,也就是任務流。這時可以根據構建後工程的目標使用者來劃分,把任務流分爲開發環境構建任務流和生產環境構建任務流兩種。這兩點認識很重要,所以我們把它兩單獨拎出來講解。

我們先來理解一下開發環境的構建任務流。

5. 理解開發環境的構建任務流

開發環境構建任務流構建後的工程是爲開發者服務的。開發者需要開發調試代碼,所以開發環境任務流構建的工程需要實現以下功能:

r90uZV

開發者需要不斷修改代碼查看效果,所以除了滿足功能之外,還需要加快構建速度並且自動刷新,以保證良好的使用體驗。

w8gUwf

關於 web 開發服務器 devServer

使用 web 開發服務器可以模擬像使用 nginx、tomcat 等服務器軟件一樣的線上環境,它在功能以及配置上都與 nginx 以及 tomcat 類似, 最簡單的配置就是指明資源路徑 baseUrl 以及服務啓動 ip 和端口 port 即可。在開發環境啓動本地服務時,配置代理可以在符合同源策略的情況下解決跨域問題

開發服務器除了可以模擬線上環境之外,更加強大的一點是它可以監聽源代碼,實現熱部署和自動刷新功能

好的,下面我們再來理解一下生產環境的構建任務流。

5. 理解生產環境的構建任務流

生產環境構建任務流構建後的工程是爲用戶服務的。與開發環境相比,它也需要語法檢查以及編譯功能,但不需要考慮修改以及調試代碼的問題,它關注的是瀏覽器兼容以及運行速度等問題。

PcVmNy

生產環境的優化除了資源的下載速度之外,還可以從很多方面入手,下面是其中的一些方面以及實現方案。

AuPzZ7

終於把任務以及任務流淺顯粗陋的講完了,接下來我們先是使用 npm scripts 來實現簡單項目的自動化構建,而後學習一下 Gulp 工具如何實現複雜項目的自動化構建

二:實現前端工程的自動化構建:npm script 方式

前面說到,完成前端工程構建也就是完成任務流。任務流由任務組成,而任務又由腳本代碼實現。

對於任務的調用,我們在定義好任務腳本或者安裝好所需的 cli 模塊之後,我們只需在 package.json 的 scripts 選項中配置一條 script,就可以方便地調用任務腳本或者 cli 模塊。

cli 模塊提供了腳本命令,可以使用 npm/npx/yarn 運行該模塊所提供的腳本。

這裏不得不提一下 node_modules/.bin 文件夾,我們在項目中安裝的 cli 模塊都會有一個 cmd 文件出現在這裏。當我們在項目中需要調用這些 cli 模塊時,只需 yarn/npx cli 模塊名的方式就可以很方便的調用這些 cli 模塊。

對於任務流的調用,我們則可以藉助一些可以幫助任務組合(並行和串行)的庫,而後在 npm script 中配置一條組合任務,調用它以啓動構建任務流。

好的,通過上面的分析之後,我們接下來展開講述一下 npm scripts 如何實現任務以及任務流的構建。

1. 單任務註冊調用示例

下面是 sass 轉換和 ES6 轉換的兩個單任務示例:

  "scripts": {
    "sass": "sass scss/main.scss css/style.css",
    "es6": "babel es6 --out-dir es5",
  },
  "devDependencies": {
    "@babel/cli": "^7.12.8",
    "@babel/core": "^7.12.9",
    "sass": "^1.29.0"
  }
# sass轉換
yarn sass
# es6轉換
yarn es6

最基本的腳本調用也就是用某個命令(如 node)去執行一個文件,這裏直接使用 yarn 就可以觸發 script 中的任務難免會讓人有點疑惑。下面是我爲您整理的任務調用追溯,理解它有時候能幫助你定位解決一些問題。

2. 簡單任務流構建示例

這裏需要再重申一點,任務流不等同於任務組合,它與構建目的有關。這裏我們假設我們前端工程的構建目的就只是 sass 轉換和 es6 轉換,那麼我們以如下形式實現自動化構建。

注意:對於任務流中的任務組合我們這裏通過 npm-run-all 這個庫來幫助我們實現,這個庫提供了兩個 cmd 文件,nun-p.cmd 實現任務的並行,nun-s.cmd 實現任務的串行。

  "scripts": {
    "sass": "sass scss/main.scss css/style.css",
    "es6": "babel es6 --out-dir es5",
    "build": "run-p sass es6"
  },
  "devDependencies": {
    "npm-run-all": "^4.1.5",
    "@babel/cli": "^7.12.8",
    "@babel/core": "^7.12.9",
    "sass": "^1.29.0"
  }
yarn build

3. 具體工程構建示例

下圖即是我們想要構建的簡單前端項目:

這個項目很簡單,它只包含一個 html 文件,一個使用了 ES6 語法 js 文件以及一個使用了 sass 語法的樣式文件,接下來我們就用 npm script 來實現這個簡單項目的自動化構建(也即開發環境構建任務流和生產環境構建任務流)。

簡單項目的自動化構建就是 npm script 實現自動化構建的使用場景。

與日常開發一樣,我們這裏也把這個工程構建分爲兩個部分,即開發環境構建和生產環境構建。下面先講講開發環境構建的實現:

(一):開發環境構建工程

通過上面我們對開發環境構建任務流的認識,我們先理一理在這個項目中,開發環境任務流至少應該包含哪些任務:

對於 sass 和 ES6 修改源代碼後的實時轉換,我們可以通過加上一個 watch 參數實現。而對於所有這些需要監聽變化的文件,我們則統一放入 temp 文件夾下(角色好比如 nginx 和 Tomcat 的應用存放目錄),而後讓 web 開發服務器監聽這個 temp 文件夾下所有文件的變化,一旦變化即重啓並刷新瀏覽器。

好的經過上面任務分析之後,我們可能會把 package.json 的 scripts 以及 devDependencies 寫成如下樣子(核心關注 scripts 中的 start 命令):

  "scripts": {
    "sassDev": "sass scss/main.scss temp/css/style.css --watch",
    "babelDev": "babel es6/script.js --out-dir temp/es5/script.js --watch",
    "copyHtmlDev": "copyfiles index.html temp",
    "serve": "browser-sync temp --files \"temp\"",
    "start": "run-p sassDev babelDev copyHtmlDev serve"
  },
  "devDependencies": {
    "@babel/cli": "^7.12.8",
    "@babel/core": "^7.12.9",
    "browser-sync": "^2.26.13",
    "copyfiles": "^2.4.1",
    "npm-run-all": "^4.1.5",
    "sass": "^1.29.0"
  }

(二):生產環境構建工程

通過上面我們對生產環境構建任務流的認識,我們先理一理在這個項目中,生產環境任務流應該包含哪些任務:

好的經過上面任務分析之後,我們可能會把 package.json 的 scripts 以及 devDependencies 寫成如下樣子(核心關注 scripts 中的 build 命令):

  "scripts": {
    "sass": "sass scss/main.scss dist/css/style.css",
    "babel": "babel es6 --out-dir dist/es5",
    "copyHtml": "copyfiles index.html dist",
    "build": "run-p sass babel copyHtml"
  },
  "devDependencies": {
    "@babel/cli": "^7.12.8",
    "@babel/core": "^7.12.9",
    "browser-sync": "^2.26.13",
    "copyfiles": "^2.4.1",
    "npm-run-all": "^4.1.5",
    "sass": "^1.29.0"
  }

上述代碼實現不全,按道理說,在生產環境下,至少需要做代碼的兼容以及壓縮。這時我們就需要找到對應的工具庫或者自己實現,另外對於壓縮而言至少需要在編譯之後完成,所以需要注意多個任務間的關係。思路很簡單,我偷個懶當前就不花時間去實踐了,需要時再實現就行。

4.npm script 構建總結

在進入 Gulp 實現前端工程的自動化構建之前,我覺得有必要再重申一點:在項目以及構建需求不復雜時,npm scripts 就可以滿足我們的構建需求了,無需藉助其它工具。

好的,下面進入 gulp 方式實現前端工程的自動化構建。

三:實現前端工程的自動化構建:gulp 方式

有些看官可能不太瞭解 gulp,這裏我先簡單介紹一下它吧。

Gulp 是一個基於流的自動化構建工具,相比較於 Grunt,它的構建速度更快,任務編寫也更加簡單靈活。

安裝好 gulp 之後,使用 gulp 的流程也就基本是如下這樣:

有了以上了解之後,下面我們就探討如何藉助 gulp 這個工具來實現自動化構建吧。

1.gulp 完成各種任務調度

(1): 實現同步任務和異步任務

對於新版本的 Gulp 來說,所有任務都是異步任務,所以任務需要告訴 Gulp 什麼時候執行結束。以下是 gulp 異步任務實現的幾種方式(關注它們是如何通知 Gulp 異步任務結束的)。

// 方式1:調用done方法主動通知任務結束
exports.foo = done ={
  console.log('foo task working~')
  done() // 標識任務執行完成
}
// 方式2:返回Promise,通過它的resolve/reject方法通知任務結束
const timeout = time ={
  return new Promise(resolve ={
    setTimeout(resolve, time)
  })
}
// 方式3:返回讀取流對象,流完即自動通知任務結束
exports.stream = () ={
  const read = fs.createReadStream('yarn.lock')
  const write = fs.createWriteStream('a.txt')
  read.pipe(write)
  return read
}
// 更多方式

(2): 實現並行任務和串行任務

並行任務和串行任務可以通過 gulp 提供的 series(串行), parallel(並行)實現。

const { series, parallel } = require('gulp');

const task1 = done ={
  setTimeout(() ={
    console.log('task1 working~');
    done();
  }, 1000)
}

const task2 = done ={
  setTimeout(() ={
    console.log('task2 working~');
    done();
  }, 1000)  
}

exports.bar = parallel(task1, task2); // 並行任務bar

exports.foo = series(task1, task2); // 串行任務foo

(3): gulp 插件任務

Gulp 生態中有很多成熟的 gulp 任務插件,使用它們可以很好地提高效率,如以下示例:

const { src, dest } = require('gulp');
const cleanCSS = require('gulp-clean-css');
const rename = require('gulp-rename');

exports.default = () ={
  return src('src/*.css')
    .pipe(cleanCSS())
    .pipe(rename({ extname: '.min.css' }))
    .pipe(dest('dist'))
}

(4): 自定義 gulp 任務

如果需要定製任務,或者對於我們的需求沒有較好的 gulp 插件,那麼我們就需要自定義任務,如下示例:

const fs = require('fs')
const { Transform } = require('stream')

exports.default = () ={
  const readStream = fs.createReadStream('normalize.css');
  const writeStream = fs.createWriteStream('normalize.min.css');
  // 文件轉換流
  const transformStream = new Transform({
    // 核心轉換過程
    transform: (chunk, encoding, callback) ={
      const input = chunk.toString();
      const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '');
      callback(null, output);
    }
  })

  return readStream
    .pipe(transformStream) // 轉換
    .pipe(writeStream) // 寫入
}

2.gulp 完成構建任務流

如下 gulpfile 文件,分爲開發環境構建(develop 任務)和生產環境構建(build 任務)。相對於理論認識,工具的使用只是不同實現方式,這裏就不多贅述了,具體的邏輯可以看下面代碼中的註釋,我爲您寫的很清楚了哦。

const {
  src,
  dest,
  parallel,
  series,
  watch
} = require('gulp')

const del = require('del')
const browserSync = require('browser-sync')

const loadPlugins = require('gulp-load-plugins')

const plugins = loadPlugins()
const bs = browserSync.create()

const data = {
  menus: [{
      name: 'Home',
      icon: 'aperture',
      link: 'index.html'
    },
    {
      name: 'Features',
      link: 'features.html'
    },
    {
      name: 'About',
      link: 'about.html'
    },
    {
      name: 'Contact',
      link: '#',
      children: [{
          name: 'Twitter',
          link: 'https://twitter.com/w_zce'
        },
        {
          name: 'About',
          link: 'https://weibo.com/zceme'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/zce'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}
// css編譯  src => temp
const style = () ={
  return src('src/assets/styles/*.scss'{
      base: 'src'
    })
    .pipe(plugins.sass({
      outputStyle: 'expanded'
    }))
    .pipe(dest('temp'))
    .pipe(bs.reload({
      stream: true
    }))
}
// js編譯   src => temp
const script = () ={
  return src('src/assets/scripts/*.js'{
      base: 'src'
    })
    .pipe(plugins.babel({
      presets: ['@babel/preset-env']
    }))
    .pipe(dest('temp'))
    .pipe(bs.reload({
      stream: true
    }))
}

// html模板解析     src => temp
const page = () ={
  return src('src/*.html'{
      base: 'src'
    })
    .pipe(plugins.swig({
      data,
      defaults: {
        cache: false
      }
    })) // 防止模板緩存導致頁面不能及時更新
    .pipe(dest('temp'))
    .pipe(bs.reload({
      stream: true
    }))
}

// 串行編譯、模板解析
const compile = parallel(style, script, page)

// 開發環境開發服務器
const serve = () ={
  watch('src/assets/styles/*.scss', style)
  watch('src/assets/scripts/*.js', script)
  watch('src/*.html', page)
  // watch('src/assets/images/**', image)
  // watch('src/assets/fonts/**', font)
  // watch('public/**', extra)
  watch([
    'src/assets/images/**',
    'src/assets/fonts/**',
    'public/**'
  ], bs.reload)

  bs.init({
    notify: false,
    port: 2080,
    // open: false,
    // files: 'dist/**',
    server: {
      baseDir: ['temp''src''public'],
      routes: {
        '/node_modules''node_modules'
      }
    }
  })
}

// 開發環境構建流:編譯 + 啓動開發服務器    src => temp
const develop = series(compile, serve)



// 生產環境下清空文件夾
const clean = () ={
  return del(['dist''temp'])
}
// 生產環境js、css、html壓縮後構建  temp => dist
const useref = () ={
  return src('temp/*.html'{
      base: 'temp'
    })
    .pipe(plugins.useref({
      searchPath: ['temp''.']
    }))
    // html js css
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
      collapseWhitespace: true,
      minifyCSS: true,
      minifyJS: true
    })))
    .pipe(dest('dist'))
}
// 生產環境圖片壓縮後構建   src => dist
const image = () ={
  return src('src/assets/images/**'{
      base: 'src'
    })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}
// 生產環境字體壓縮後構建   src => dist
const font = () ={
  return src('src/assets/fonts/**'{
      base: 'src'
    })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}
// 生產環境靜態資源構建
const extra = () ={
  return src('public/**'{
      base: 'public'
    })
    .pipe(dest('dist'))
}

// 上線之前執行的任務   src =(temp =>) => dist 
const build = series(
  clean,
  parallel(
    series(compile, useref),
    image,
    font,
    extra
  )
)

module.exports = {
  clean,
  build,
  develop
}

四:其它方式實現前端工程的自動化構建

在前端工程的自動化構建發展歷程中,出現了很多的自動化工具。如 grunt、gulp、webpack,包括現在正處在風口浪尖的 vite 等等。webpack 的相關內容會在另外一篇文章中探討,對於其它的構建工具以及過時的工具我覺得沒有現在學習的必要,具體生產需要使用時學習即可。

總的來說,工具很多,但是最重要的其實是對於自動化構建本身的理論認識,而對於自動化構建的實現及其工具而言,我覺得,包括比較需要掌握的 gulp 和 webpack,完全理解 npm script 方式是更重要的。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/BzyrwQIYiOJq_tSiB9pLgg