Webpack自動化構建實踐指南

熊建剛發表於2019-03-03

由於現在的部落格是使用wordpress搭建,自己得經常修改過一些程式碼,但是修改第三方原始碼真的比較痛苦,於是決定計劃開始使用React + Node.js / Python開發新部落格專案,最終替換當前部落格程式碼,方便以後部落格的維護和更新,也能實現自我開發技術,架構設計,解決問題能力的提升,同時記錄下整個開發歷程,總結,分享,希望能與讀者們一起進步。本篇介紹如何使用Webpack和Babel,Eslint,documentation.js等搭建專案開發環境和生產環境,也算專案的準備工作,下一期計劃介紹專案的架構設計和技術棧選擇。

點此檢視專案原始碼地址

我的個人部落格

npm VS Yarn

在本專案我們使用Yarn管理專案三方依賴,不過放心,Yarn和NPM不衝突,也不是要替代NPM,使用方式基本一致,只需要簡單瞭解以下幾點。

三方庫版本管理

npm 和 Yarn 都使用 package.json 來跟蹤專案的依賴,版本號並非一直準確,因為你可以定義版本號範圍,npm的不同更新範圍,可能導致在擁有相同 package.json 檔案的機器上安裝不同版本包,這可能導致一些差異的異常和衝突。

那npm有解決方式嘛?npm中可以使用 npm shrinkwrap生成一個版本鎖檔案npm-shrinkwrap.json,在 npm install 時會在讀取 package.json 前先讀取這個檔案,但是當更新包版本時,版本鎖檔案並不會自動更新,我們得手動再次執行npm shrinkwrap命令更新它。

那麼Yarn有什麼優勢呢?每次新增或更新安裝庫包時,Yarn 都會建立(或更新)yarn.lock 檔案,這樣可以確保所有機器安裝相同版本包,同時支援 package.json 中定義的允許版本範圍,和npm的區別在於Yarn總會自動更新 yarn.lock,而npm需要手動更新。

併發安裝

npm通常是按順序一個一個安裝依賴,而Yarn支援並行載入安裝多個三方庫包,所有其速度和效率都更快。

離線快取

使用Yarn管理包時,三方庫包存放在本地磁碟,下次安裝將直接使用本地檔案而不是再次下載,這也從另一方面使其安裝速度優於npm。

簡而言之就是,Yarn和npm使用方式幾乎一樣,但是其版本管理更方便,安裝速度更快,更有優勢,但是實際上它的所有三方庫包載入地址和npm都是統一的。

Webpack

我們使用Webpack打包工具作為專案的自動化構建工具,將JavaScript,CSS,圖片等資源都當作JavaScript模組(使用Webpack loader處理轉換)進行統一管理,關於Webpack博主之前總結過兩篇文章,可以參考:

  1. Webpack搭建SPA應用開發環境
  2. Webpack模組化管理CSS和圖片等資源

有了前文的鋪墊,本文就不打算展開介紹Webpack的工作原理和具體配置,而計劃從專案實踐開發和測試,打包層面思考如何更好的組織Webpack,如何使用Webpack提告專案開發,打包效率。

Webpack配置檔案

首先我們在根目錄下建立webpack.config.js配置檔案:

module.exports = function () {
  let env
  let _DEV_ = true // 開發環境
  let _PROD_ = false // 生產環境
  
  switch (process.env.NODE_ENV) {
    case `dev`:
      env = `dev`
      _DEV_ = true
      _PROD_ = false
      break
    case `production`:
      env = `prod`
      _DEV_ = false
      _PROD_ = true
      break
    default:
      env = `dev`
      _DEV_ = true
      _PROD_ = false
  }
  // 根據環境引數動態決定引入對應配置檔案
  return require(`./webpack/${env}.conf.js`)({
    ROOTPATH: __dirname,
    _DEV_,
    _PROD_
  })
}
複製程式碼

根據process.env.NODE_ENV環境引數動態決定載入對應配置檔案:

  1. dev:載入webpack/env.conf.js配置檔案;
  2. prod:載入webpack/prod.conf.js配置檔案;

我們在專案根目錄下建立了webpack目錄,其內建立了三個配置檔案:

  1. base.conf.js:基礎配置檔案,在開發,生產環境都需要的配置;
  2. dev.conf.js:開發環境配置檔案;
  3. prod.conf.js:生產環境打包配置檔案;

開發環境配置

開發環境配置檔案中定義了一些開發使用的構建配置,然後引入基礎配置檔案,使用webpack-merge三方庫,將開發環境配置合併至基礎配置物件,然後返回開發環境打包構建配置物件,作為Webpack打包構建的引數:

const webpackMerge = require(`webpack-merge`)
const PUBLICPATH = `/assets/`
const PORT = `9090`
let options = { /* ... */ }
module.exports = function (args) {
  options.ROOTPATH = args.ROOTPATH
  options.env = args.env
  return webpackMerge(require(`./base.conf`)(options), {
    devtool: `source-map`,
    devServer: {
      contentBase: path.join(args.ROOTPATH, `./src`),
      historyApiFallback: true,
      inline: true,
      hot: true,
      port: PORT,
      proxy: {
        `*`: `http://localhost:${PORT}/${PUBLICPATH}/`
      }
    },
    plugins: []
  })
}
複製程式碼

生產環境配置

生產環境配置檔案中定義了的是生產環境使用的構建配置,然後也是引入基礎配置檔案,使用webpack-merge三方庫,將生產環境配置合併至基礎配置,然後返回配置物件,作為Webpack打包構建的引數:

let options = { /* ... */}
module.exports = function (args) {
  options.ROOTPATH = args.ROOTPATH
  options.env = args.env
  return webpackMerge(require(`./base.conf`)(options), {
    plugins: [
      new webpack.DefinePlugin({
        `process.env`: `production`
      }),
      // 生成獨立css檔案
      new ExtractTextPlugin({
        filename: `css/[name].css`
      }),
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false
        }
      })
    ]
  })
}
複製程式碼

指令

然後就是為不同環境配置可執行指令,我們使用npm scripts方式,在package.json檔案中配置執行指令:

{
  "scripts": {
    "start": "cross-env NODE_ENV=dev webpack-dev-server",
    "build": "cross-env NODE_ENV=production webpack"
  }
}
複製程式碼
  1. start:開發環境執行指令,使用cross-env三方庫設定process.env.NODE_ENVdev,並在本地開啟webpack開放伺服器,方便開放;
  2. build:生產環境執行指令,使用cross-env三方庫設定process.env.NODE_ENVproduction,將打包輸出程式碼和資原始檔;

最後分別執行yarn startyarn build指令即可分別執行開發和生產構建打包了。

Babel

可自定義配置型的通用編譯器,需要明確指定期望babel做什麼,通過安裝**外掛(plugins)預設(presets,也就是一組外掛)**來指示 Babel 去做什麼事情。

配置檔案

首先需要建立一個配置檔案,即在專案的根路徑下建立 .babelrc 檔案。然後輸入以下內容作為開始:

{
  "presets": [],
  "plugins": []
}
複製程式碼

之後就可以擴充這個配置檔案以指定此專案中 Babel 的功能。

babel-preset-es2015

我們期望在專案中能使用更新潮的ES6版本語法,但由於目前還有許多JavaScript環境不能很好相容ES6,所以需要Babel將ES6程式碼編譯成ES5語法程式碼,保證應用的使用範圍。

執行如下命令,安裝 “es2015” Babel 預設:

yarn add --dev babel-preset-es2015
複製程式碼

修改.babelrc配置檔案:

{
  "presets": [
    "es2015"
  ],
  "plugins": []
}
複製程式碼

babel-preset-stage-num

另外,JavaScript還有一些提案,正在推進,不久的將來也可能成為標準的一部分,所以目前將這些草案提出,內容更新直至最終成為標準,新增進標準庫的過程劃分為 5(0-4)個階段。 根據提案的狀態和內容,將其在各個階段更新(階段0至階段3),最終在階段 4表明該提案被標準正式採納,當然不被採納的提案不會進入階段4。

以下是4個不同階段的打包預設:

  • babel-preset-stage-0
  • babel-preset-stage-1
  • babel-preset-stage-2
  • babel-preset-stage-3

注: stage-4 預設不存在,它其實就是上文介紹的 es2015 預設。

以上每種預設都包含緊隨的後期階段預設,同時還可能包含其他額外特性。例如,babel-preset-stage-0 包含 babel-preset-stage-1babel-preset-stage-2babel-preset-stage-3,而 babel-preset-stage-1則包含 babel-preset-stage-2babel-preset-stage-3依次後推。

點此檢視關於各階段預設的詳細特性內容文件

我們次選擇支援特性最全面的預設:

yarn add --dev babel-preset-stage-0
複製程式碼

.babelrc 配置檔案內新增:

{
  "presets": [
    "es2015",
    "stage-0"
  ],
  "plugins": []
}
複製程式碼

babel-preset-react

我們的專案期望使用React開發,所以需要擴充支援React/JSX語法,安裝預設:

yarn add --dev babel-preset-react
複製程式碼

.babelrc 配置檔案內新增:

{
  "presets": [
    "es2015",
    "stage-0",
    "react"
  ],
  "plugins": []
}
複製程式碼

babel-polyfill

至此,使用Babel,我們的·專案幾乎可以支援所有的ES6及ES7語法,但是對於新增的JavaScript API是無能為力的,如Symbol這種新API,並不是通過語法轉換能實現的,所以我們需要另外的方式解決。

業內提出了Polyfill(填充),以新增額外程式碼的方式使得當前執行環境支援不存在的原生Api ,擴充了尚處在推進階段的API的使用範圍。

yarn add babel-polyfill
複製程式碼

此處不需要新增--dev引數。

然後在檔案入口引入即可:

import "babel-polyfill";
複製程式碼

babel-runtime

前面提到的Babel通過轉換語法以支援我們以ES6等更新的語法方式開發程式碼,這時Babel會在每一個處理的檔案頭部注入輔助程式碼,會產生很多冗餘,重複性的內容,導致程式碼量暴增,所以我們需要將這些輔助程式碼抽取至一個統一環境,Babel提供的就是執行時(runtime)環境。

要實現Babel執行時環境,需要安裝 babel-plugin-transform-runtimebabel-runtime

yarn add --dev babel-plugin-transform-runtime babel-runtime
複製程式碼

然後更新 .babelrc

{
  "plugins": [
    "transform-runtime",
  ]
}
複製程式碼

按需載入(babel-plugin-import)

很多時候,我們開發業務並不需要自制UI,會選擇一些開源元件庫以快速開發實現產品,如antd,weui,material-ui等,我們可以選擇直接提前載入三方庫所有模組,但是很多時候我們希望能實現按需載入,減少初始程式碼包的體積,這時,我們可以在babel配置檔案中宣告按需載入該第三方庫,當然首先得安裝外掛babel-plugin-import

yarn add --dev babel-plugin-import
複製程式碼

然後在配置檔案.babelrc中新增配置:

{
  "plugins": [
    "import",
    {
      "style": "../styles", // 載入樣式解析方式,(值為true時,可能是less/Sass),此處值設為相對libraryName具體模組請求路徑值
      "libraryDirectory": "", // 載入包的目錄,(預設是lib,即node_modules/lib/)
      "libraryName": "material-ui" // 載入三方元件庫名,當然另外需要安裝該三方庫
    }
  ]
}
複製程式碼

此時,webapck loader處理css時不能新增exclude: /node_modules/

其他外掛

我們還可以根據專案實際需求和愛好自定義安裝外掛,更多資訊檢視官方外掛文件

在這裡推薦一款babel-pliugin-transform-s2015-classes外掛擴充以實現JavaScript內建class物件的extends繼承特性,參考文件ES2015 classes transform

yarn add --dev babel-plugin-transform-es2015-classes
複製程式碼

.babelrc檔案內新增plugins內容:

{
  "plugins": [
    "transform-runtime",
    "transform-es2015-classes",
    [
      "import",
      {
        "style": "css",
        "libraryDirectory": "",
        "libraryName": "material-ui"
      }
    ]
  ]
}
複製程式碼

語法檢測(Eslint)

為了保證程式碼質量,統一程式碼風格是很重要的,而只靠團隊口頭約定明顯是不能盡如人意,所以通常需要在自動化構建層面進行程式碼語法檢測,有很多語法檢測工具如jslint,eslint,目前使用率最高的要數eslint了,所以我們的專案也引入eslint,首先安裝依賴:

yarn add --dev eslint
複製程式碼

更多細節參考配置文件,下文簡要介紹主要。

配置檔案

然後在專案根目錄下建立eslint配置檔案.eslintrc,內容是一個物件:

{}
複製程式碼

解析器(parser)

另外,ESLint 預設使用Espree作為其解析器,你可以在配置檔案中指定一個不同的解析器,如babel-eslint,esprima等,我們專案使用babel-eslint:

yarn add --dev babel-eslint
複製程式碼

在配置檔案內新增parser屬性:

{
  "parser": "babel-eslint"
}
複製程式碼

eslint-plugin-babel

eslint還支援可選安裝外掛,擴充eslint,如eslint-plugin-babel,該外掛與babel-eslint協作,使得eslint可以更好的與babel同時工作,更多請檢視參考文件

yarn add --dev eslint-plugin-babel
複製程式碼

在配置檔案新增宣告:

{
  "plugins": [
    "babel"
  ],
}
複製程式碼

aslant-plugin-react

eslint預設是檢測JavaScript語言語法的,而對於React/JSX這類包含其自定義語法和語法糖的框架而言,需要另外擴充安裝外掛才能和eslint結合使用,所以使用eslint檢測React特定語法需要安裝eslint-plugin-react外掛:

yarn add --dev eslint-plugin-react
複製程式碼

新增配置檔案:

{
  "plugins": [
    "babel",
    "react"
  ]
}
複製程式碼

擴充(extends)

除了自定義語法檢查規則外,我們可以使用Eslint提供的整合擴充包,使用共享的語法檢測配置物件,如eslint-config-standardeslint-config-standard-react

yarn add --dev eslint-config-standard eslint-config-standard-react eslint-plugin-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-node eslint-plugin-react
複製程式碼

注:這裡包含了上一小節提到的eslint-plugin-react是為了支援eslint-config-standard-react配置包。

然後在.eslintrc配置檔案中新增擴充:

{
  "extends": [
    "standard",
    "standard-react"
  ]
}
複製程式碼

若不想使用這類整合語法檢測規則,可以移除配置檔案中內容並移除依賴:

yarn remove eslint-config-standard eslint-config-standard-react eslint-plugin-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-node eslint-plugin-react
複製程式碼

語法規則(rules)

要新增語法規則,只需要宣告在rules屬性物件中,如:

{
  "rules": {
    "strict": 0,
    "semi": 2, // 強制語句末尾新增符號,否則報錯
    "quotes": [
      1,
      "single"
    ],
  }
}
複製程式碼

規則結構

當宣告語法檢測規則時,需要設定規則 ID為以下值之一:

  • "off"0 – 關閉規則
  • "warn"1 – 開啟規則,使用警告級別的錯誤:warn (不會導致程式退出)
  • "error"2 – 開啟規則,使用錯誤級別的錯誤:error (當被觸發的時候,程式會退出)
{
  "rules": {
  	eqeqeq: 0, // or "off"
  	curly: 2 // or "error"
  }
}
複製程式碼

某些規則還可能有額外的配置選項,可以使用陣列指定,如:

{
  "rules": {
    "eqeqeq": "off",
    "curly": "error",
    "quotes": ["warn", "single"] // 開啟使用單引號,若使用雙引號將發出警告
  }
}
複製程式碼

指令

要執行語法檢測,只需要執行./node_modules/.bin/eslint src(專案本地安裝eslint,而非全域性安裝,則需要指定執令指令碼路徑),將會遍歷檢查src目錄下的所有原始碼檔案語法並輸出結果,當然我們最終需要將指令根據npm scripts規範插入package.json檔案:

{
  "scripts": {
    "lint": "eslint --cache --fix src"
  }
}
複製程式碼

使用npm scripts執行指令時,無論專案本地安裝還是全域性安裝,都可以省略指令指令碼路徑,因為npm將自動匹配可用路徑。

文件

一個優秀的專案當然少不了文件,文件可以幫助其他開發者快速瞭解整個專案內容及進度,也有助於bug修復時查詢內容,追蹤溯源,所以文件是有必要的,於是通過調研發現了JSdoc和documentation.js幫助自動化產出API文件。

documentation

JSdoc一樣,documentation也是根據程式碼註釋自動構建出專案文件,前提是我們的程式碼註釋必須按照其規範指南,詳情參考JSdoc文件

我們首先安裝documentation.js:

yarn add --dev documentation
複製程式碼

指令

然後可以執行指令:

./node_modules/.bin/documentation build src/app.js -f md > API.md
複製程式碼

會發現在根目錄輸出了API.md檔案。

我們在package.json檔案中配置npm scripts執行指令碼:

"scripts": {
  "doc": "./node_modules/.bin/documentation build src/app.js -f md > API.md"
}
複製程式碼

專案本地安裝documentation時,直接在命令列終端執行指令時需要指定./node_modules/.bin/documentation路徑,若全域性安裝則只可直接使用documentation指令。而執行package.json中的腳步,可以直接簡寫,npm將為我們自動匹配。

點此檢視專案原始碼地址

相關文章