webpack 快速入門 系列 —— 實戰一

彭加李發表於2021-05-16

實戰一

準備本篇的環境

雖然可以僅展示核心程式碼,但筆者認為在一個完整的環境中邊看邊做,舉一反三,效果更佳。

這裡的環境其實就是初步認識 webpack一文完整的示例,包含 webpack、devServer、處理css、生成 html。

專案結構如下:

webpack-example2        
  - src                 // 專案原始碼
    - a.css
    - b.js
    - c.js
    - index.html        // 頁面模板
    - index.js          // 入口
  - package.json        // 存放了專案依賴的包
  - webpack.config.js   // webpack配置檔案

src中的程式碼如下:

// a.css
body{color:blue;}

// b.js
import './c.js'
console.log('moduleB')
console.log('b2')

// c.js
console.log('moduleC')

// 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=`, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <p>請檢視控制檯</p>
</body>
</html>

// index.js
import './b.js'
import './a.css'
console.log('moduleA')

package.json:

{
  "name": "webpack-example2",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "dev": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "css-loader": "^5.2.4",
    "html-webpack-plugin": "^4.5.2",
    "style-loader": "^2.0.0",
    "webpack": "^4.46.0",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.2"
  }
}

webpack.config.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"]
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
        template: 'src/index.html'
    })
  ],
  mode: 'development',
  devServer: {
    open: true,
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9000,
  },
};

在 webpack-example2 目錄下執行專案:

// 安裝專案依賴的包
> npm i
// 啟動服務
> npm run dev

啟動伺服器後,瀏覽器會自動開啟頁面,如果看到藍色文字”請檢視控制檯“,說明環境已準備就緒。

打包樣式

處理 css 和 less

less 是一種 css 預處理語言,在 webpack 中要處理 less 需要使用 less-loader,用於將 less 轉為 css。

首先安裝依賴,然後修改配置檔案:

// 安裝包。版本8安裝失敗,所以降了一個版本
> npm i -D less-loader@7

// webpack.config.js
// 增加對 less 檔案處理的loader
rules: [
    // 需要保留,否則識別不了 css 檔案
    {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"]
    },
    // +
    {
        test: /\.less$/i,
        loader: [
            // compiles Less to CSS
            "style-loader",
            "css-loader",
            "less-loader",
        ],
    },
],

然後增加 a.less 檔案,在 index.js 中引入 a.less,重新啟動服務進行測試:

// src/a.less
body{
    p{
        color:pink;
    }
}

// index.js
import './b.js'
import './a.css'
// +
import './a.less'
console.log('moduleA')

// 啟動服務
> npm run dev 

在新開的頁面中,看到粉色文字”請檢視控制檯“,說明 less 處理成功。

提取 css 成單獨檔案

通過瀏覽器我們發現現在 css 是嵌在頁面內的,就像這樣:

<head>
  ...
  <style>body{color:blue;}</style>
  <style>body p {
    color: pink;
  }
  </style>
</head>

通常我們會通過 link 來引入 css 檔案,所以接下來就將 css 取成單獨的檔案。這裡需要使用 mini-css-extract-plugin 這個包。

我們只需要安裝依賴包,修改配置檔案即可:

// 安裝依賴包 
> npm i -D mini-css-extract-plugin@1
// 不在需要 style-loader,解除安裝
> npm r -D style-loader

// webpack.config.js
// +
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  ...
  module: {
    rules: [
      // 修改規則
      {
        test: /\.css$/i,
        // 將 style-loader 改為 MiniCssExtractPlugin.loader
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
      {
        test: /\.less$/i,
        loader: [
          // 將 style-loader 改為 MiniCssExtractPlugin.loader
          MiniCssExtractPlugin.loader,
          "css-loader",
          "less-loader",
        ],
      },
    ],
  },
  plugins: [
    // +
    new MiniCssExtractPlugin(),
    ...
  ],
  
};

啟動服務(npm run dev),在開啟的頁面中可以看到 css 已經改為 link 的方式引入,就是這樣:

// 從 style 改為 link 方式
<link href="main.css" rel="stylesheet">

// 通過網路檢視 main.css 的內容是:
body{color:blue;}
body p {
  color: pink;
}

由於我們對 css 和 less 都使用了 MiniCssExtractPlugin.loader,所以 a.css 和 a.less 都被提取到 main.css 中。

Tip:如果通過npm run build打包,則可以看到 dist/main.css 檔案。

使用 PostCSS

PostCSS - 使用JavaScript轉換CSS的工具。

可以將 postcss 當作一個平臺,下面我們通過 postcss 做兩件事:

  • 增加程式碼可讀性(或增加字首)
:fullscreen {
}

// 轉為

:-webkit-full-screen {
}
:-ms-fullscreen {
}
:fullscreen {
}
  • 立即使用明天的CSS
body {
  color: lch(53 105 40);
}

// 轉為

body {
  color: rgb(250, 0, 4);
}

webpack 可以通過 postcss-loader 來使用 postcss。

由於 postcss 只是一個平臺,具體功能需要通過外掛來實現,這裡我們使用 postcss-preset-env

postcss-preset-env 可以將現代CSS轉換為大多數瀏覽器可以理解的內容,並根據目標瀏覽器或執行時環境確定所需的polyfill。而且它包含自動字首。

首先安裝相關依賴,並修改配置檔案:

> npm i -D postcss-loader@4 postcss-preset-env@6

// webpack.config.js
// +
const postcssPresetEnv = require('postcss-preset-env');
// +
const postcssLoader = { 
  loader: 'postcss-loader', 
  options: {
    // postcss 只是個平臺,具體功能需要使用外掛
    // Set PostCSS options and plugins
    postcssOptions:{
      plugins:[
        // 配置外掛 postcss-preset-env
        [
          "postcss-preset-env",
          {
            // 自動字首。預設是true
            // autoprefixer: true,
            // 根據您所支援的瀏覽器來確定需要哪些polyfill。這裡僅做演示
            browsers: 'ie >= 8, chrome > 10',
            // stage 預設是 2
            // stage:2
          },
        ],
      ]
    }
  } 
}

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          MiniCssExtractPlugin.loader, 
          "css-loader",
          // + 放在css-loader後面
          postcssLoader
        ]
      },
      {
        test: /\.less$/i,
        loader: [
            MiniCssExtractPlugin.loader,
            "css-loader",
            // +
            postcssLoader,
            "less-loader",
        ],
      },
    ]
  },
};

接著修改 a.css 和 a.less,重新啟動伺服器:

// a.css
body{
    color: lch(53 105 40);
}

// a.less
body{
    p{
        transform: scale(1, 2);
    }
}

// 啟動服務
> npm run dev

在新開的頁面中,我們看到紅色文字”請檢視控制檯“,而且文字縱向拉長了一倍。通過瀏覽器檢視 main.css 原始碼如下:

body{
    color: rgb(250, 0, 4);
}
body p {
  -webkit-transform: scale(1, 2);
      -ms-transform: scale(1, 2);
          transform: scale(1, 2);
}

至此,增加字首以及立即使用明天的CSS都已經完成。

Tip:stage(階段)可以是0(實驗)到4(穩定),預設是2,如果我們改為3或4,重新打包,lch(53 105 40);則不會轉為 rgb(250, 0, 4);將plugins換成下面的寫法效果相同。

plugins:[
  postcssPresetEnv({
    browsers: 'ie >= 8, chrome > 10',
  })
]

postcss-preset-env 支援任何標準的 browserslist 配置,可以是 .browserslistrc 檔案,package.json 中的browserslist 鍵或 browserslist 環境變數。

如果將 browsers: 'ie >= 8, chrome > 10', 註釋,browsers 將使用預設的 browserslist 查詢(即> 0.5%, last 2 versions, Firefox ESR, not dead),重新構建,則不會新增字首。

如果不想在 browsers 中寫,在 package.json 中的 browserslist 中配置也是可以的:

// package.json
{
  ...
  "browserslist": [
      "ie >= 8",
      "chrome > 10"
  ]
}

:package.json 不能寫註釋,本文在 package.json 中的註釋僅作說明。

如果覺得 package.json 寫的內容太多,我們甚至可以將這部分提取到一個單獨的檔案中來寫:

// .browserslistrc
// from github browserslist
# Browsers that we support

ie >= 8
chrome > 10

最後,如果我們針對開發環境和生成環境做不同的處理,比如開發環境支援 ie8+,而生產環境支援 chrom10+,我們可以這麼寫:

// .browserslistrc
# Browsers that we support

[production]
chrome > 10

[development]
ie >= 8

然後在配置檔案中通過 process.env 來指定環境:

// +
// process.env屬性返回一個包含使用者環境的物件
process.env.NODE_ENV = 'development' // or production

Browserslist將根據BROWSERSLIST_ENV或NODE_ENV變數選擇,所以設定 process.env.BROWSERSLIST_ENV 也是可以的。

再次打包 npm run build,則只會針對 ie,生成的 main.css 內容如下:

body{
    color: rgb(250, 0, 4);
}
body p {
  -ms-transform: scale(1, 2);
      transform: scale(1, 2);
}

壓縮 css

如果我們需要壓縮 css 程式碼,可以使用 optimize-css-assets-webpack-plugin,用於優化或最小化 css。

首先安裝依賴,然後修改配置:

> npm i -D optimize-css-assets-webpack-plugin@5

// webpack.config.js
// +
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

plugins: [
    new HtmlWebpackPlugin({
        template: 'src/index.html'
    }),
    new MiniCssExtractPlugin(),
    // +
    new OptimizeCssAssetsPlugin()
  ],

重新打包,原來的 main.css 則變成一行,請看:

> npm run build

// main.css(優化前)
body{
    /* 註釋 */
    color: rgb(250, 0, 4);
}
body p {
  -ms-transform: scale(1, 2);
      transform: scale(1, 2);
}

// main.css(優化後)
body{color:#fa0004}body p{-ms-transform:scaleY(2);transform:scaleY(2)}

優化後,css 變成了一行,註釋也刪除了。

打包圖片

前端資源通常有圖片,由於 webpack 只識別 javascript,所以需要 loader 來幫們識別圖片。

我們使用 url-loader,能將圖片轉為 base64。

首先安裝依賴,並修改配置檔案:

> npm i -D url-loader@4

// webpack.config.js
module: {
  rules: [
    ...
    // +
    {
      test: /\.(png|jpg|gif)$/i,
      use: [
        {
          loader: 'url-loader',
          options: {
            // 指定檔案的最大大小(以位元組為單位)
            limit: 1024*7,
          },
        },
      ],
    },
  ]
},

接著引入圖片,啟動服務:

// 引入圖片。src/6.68kb.png

// a.less
body{
    p{
        transform: scale(1, 2);
    }
    .m-box{display:block;width:100px;height:100px;}
    .img-from-less{background:url(./6.68kb.png) no-repeat;background-size:100% 100%;}
}

// index.html
...
<body>
    <p>請檢視控制檯</p>
    <span class='m-box img-from-less'></span>
</body>
...

// 啟動服務
> npm run dev

Tip: 筆者的圖片大小為 6.68kb,上面的 limit 只需要大於6.68kb即可

在新開的頁面中,我們在”請檢視控制檯“文字下面看見了我們設定的圖片。通過檢查元素會發現這張圖片是 base64。

body .img-from-less{
  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA... no-repeat;
  ...
}

如果將 limit: 1024*7 修改為 limit: 1024*6(也就是將 limit 設定的比圖片的 size 更小),再次執行 npm run dev,會發現報錯了。還會提示找不到 file-loader。這是因為這張圖片(6.68kb.png)大於 1024*6,所以就不會被打包成 base64,所以需要 file-loader 來處理。

安裝依賴包 npm i -D file-loader@6,再次啟動伺服器,頁面上又看到我們的圖片,而且這次不再是 base64,而是直接生成了一張圖片。

body .img-from-less{
  background: url(26bd867dd65e26dbc77d1e151ffd36e0.png) no-repeat;
  ...
}

圖片除了在 css 中使用,我們也會通過 img 元素引用,於是我們在 index.html 中新增 <img class='m-box' src="./6.68kb.png" alt=""> 再次啟動服務,在開啟的瀏覽器頁面中發現 img 引用的圖片沒生效,而且原始碼也沒變化。

這裡需要使用 html-loader 這個包,它能讓每個被載入的屬性(例如:<img src="image.png")能被引入(imported)。

安裝依賴包,修改配置:

> npm i -D html-loader@1

// webpack.config.js
module: {
    rules: [
      ...
      // +
      {
        test: /\.html$/i,
        loader: 'html-loader',
      },
    ]
},

再次啟動服務,就能看到兩張一樣的圖片了。img 的程式碼變為 <img class="m-box" src="26bd867dd65e26dbc77d1e151ffd36e0.png" alt="">

如果再次將 limit: 1024*6 修改為 limit: 1024*7,啟動服務你會發現這兩處圖片都變為 base64。

打包 javascript

js 語法檢查

有時我們希望團隊成員寫的 javascript 程式碼風格一致。

我們可以使用 eslint,它能查詢並修復JavaScript程式碼中的問題;可以自定義 eslint,使其完全按照專案所需的方式工作。程式碼風格,筆者選用 airbnb,一個流行的 javascript 風格指南(此刻是第 6 名(topics javascript))。

在 webpack 中使用 eslint,需要使用 eslint-webpack-plugin( eslint-loader廢棄了),而 eslint-webpack-plugin 依賴於 eslint

eslint-config-airbnb 預設匯出包含我們所有的ESLint規則,包括ECMAScript 6+和React,而 我們不需要使用 react,所以使用 eslint-config-airbnb-base 即可。

首先安裝依賴包,修改配置:

// 沒有引入 eslint-plugin-import
> npm i -D eslint@7 eslint-webpack-plugin@2 eslint-config-airbnb-base@14 

// webpack.config.js
// +
const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = {
  // ...
  plugins: [
    new ESLintPlugin({
      // 將啟用ESLint自動修復功能。此選項將更改原始檔
      fix: true
    })
    
  ],
  // ...
};

// package.json
{
  // +
  "eslintConfig": {
    "extends": "airbnb-base"
  }
}

重新打包 npm run build,出現了一些警告和錯誤,核心資訊如下:

WARNING in 

webpack-example2\src\index.js
  6:1  warning  Unexpected console statement  no-console

✖ 6 problems (2 errors, 4 warnings)

ERROR in 

webpack-example2\src\index.js
  1:8  error  Unexpected use of file extension "js" for "./b.js"  import/extensions

✖ 5 problems (2 errors, 3 warnings)

錯誤(import/extensions)是不希望使用 js 副檔名,將 ./b.js 改為 ./b 就好了,可參考issues:import/extensions

警告(no-console)是因為不能出現 console.log。可以通過配置將這個告警關閉:

// package.json
{
  "eslintConfig": {
    "extends": "airbnb-base",
    // +
    "rules": {
        "no-console": "off",
    }
  }
}

import/extensions修復,並將警告關閉,重新打包 npm run build則不會出現警告和錯誤。

Tip:打包後,原始碼也會自動修復,比如 src/index.js 中的 sum() 方法,a, 後面是多個空格,打包後會合併成一個空格:

function sum(a,    b) {
  return a + b;
}
// 需要呼叫 sum() 方法
// 否則報錯:error  'sum' is defined but never used  no-unused-vars
console.log(sum(1, 100));

// 修復後

function sum(a, b) {
  return a + b;
}

如果在 js 檔案中使用 window ,再次打包會報錯,就像這樣:

// index.js
// +
setTimeout(() => {
  window.location = 'https://www.baidu.com/';
}, 1000);

// 打包
> npm run build
...
error  'window' is not defined  no-undef

可以在配置檔案中指定環境來解決這個問題。就像這樣:

// package.json
"eslintConfig": {
    // +
    "env": {
      "browser": true
    }
}

如果不想寫到 package.json,也可以配置到單獨的檔案(.eslintrc.js)中:

// .eslintrc.js
module.exports = {
    "extends": "airbnb-base",
    "rules": {
        "no-console": "off"
    },
    "env": {
      "browser": true
    }
}

js 相容性處理

我們想使用 es6 來編寫程式碼,但有的瀏覽器支援的不夠全面,所以我們會將 es6 轉成 es5。

接著上面的例子進行,重寫 index.js,放入一個箭頭函式,再次打包,你會發現 webpack 不會對 es6 語法做處理,const 還是 const,而不是 var:

// src/index.js
const sum = (a, b) => (a + b);
console.log(sum(1, 10));

// sum 還是我們的箭頭函式
eval("const sum = (a, b) => (a + b);\nconsole.log(sum(1, 10));\n\n\n//# sourceURL=webpack:///./src/index.js?");

Babel 是一個 JavaScript 編譯器。通過它可以讓我們使用下一代的 JavaScript 語法程式設計。

在 webpack 中要使用 babel 就得用 label-loader。用法(Usage)如下:

// 安裝依賴包。沒有使用 @babel/core
> npm i -D babel-loader@8 @babel/preset-env@7

// webpack.config.js
module: {
  rules: [
    // +
    {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env']
            ]
          }
        }
    }
  ]
}

重新打包,箭頭函式就變成普通函式:

eval("var sum = function sum(a, b) {\n  return a + b;\n};\n\nconsole.log(sum(1, 10));\n\n//# sourceURL=webpack:///./src/index.js?");

如果我們在 js 中使用 Promise,重新打包後 Promise 還是 Promise,而且在不識別 Promise 語法的瀏覽器中(比如 ie11)執行會報錯。

// index.js
Promise.resolve('aaron').then((v) => {
  console.log(v);
});

// 重新打包,Promise 還是 Promise
eval("Promise.resolve('aaron').then(function (v) {\n  console.log(v);\n});\n\n//# sourceURL=webpack:///./src/index.js?");

babel 官網提到,使用@babel/polyfill,就可以使用新的內建函式(例如Promise或WeakMap),靜態方法(例如Array.from或Object.assign),例項方法(例如Array.prototype.includes)等等。所以這個 polyfill 是我們的解決方案。

但是 @babel/polyfill 廢棄了。而 @babel/polyfill 包含 regenerator runtime 和 core-js。

core-js,包括適用於2021年前ECMAScript的polyfill,而且僅載入必需的功能。

在 useBuiltIns 引數中也提到:由於在7.4.0中已棄用@ babel/polyfill,因此我們建議直接新增core-js並通過corejs選項設定版本。

於是我們知道 core-js 能解決 Promise 這類問題。

如何使用 core-js ?我們先來介紹一下外掛預設

babel 通過將外掛(或預設)應用於配置檔案來啟用Babel的程式碼轉換。比如外掛列表中的es2015,這是一個集合,包含了箭頭函式(arrow-functions)、類(classes)等外掛;

而預設(presets)其實是多個外掛(plugin)的集合。比如 @babel/preset-env 這種預設則包含了 es2015、es2016、es2017等最新的外掛。

最後根據babel-preset-env中的介紹,我們將 core-js 應用上:

// 安裝依賴
> npm i -D core-js@3.11

// webpack.config.js
{
  loader: 'babel-loader',
  options: {
    presets: [
      [
        '@babel/preset-env', 
        // +
        {
          // 配置處理polyfill的方式
          useBuiltIns: "usage",
          // 版本與我們下載的版本保持一致
          corejs: { version: "3.11"},
          "targets": "> 0.25%, not dead"
        }
      ]
    ]
  }
}

重新打包,dist/main.js 的Size變成 109 Kib,而之前還不到 4kiB。

啟動服務,在不支援 Promise 語法的瀏覽器中,比如 ie11,也能在控制檯輸入 aaron。

至此 javascript 的相容處理完畢。

相關文章