Webpack 實作入門2:打包 CSS / SCSS 與 加入 Bootstrap


作者: | 2017/08/21 | 1 則迴響


前篇教學在: https://audilu.com/2017/08/21/webpack-tutorial/
範例檔在:https://github.com/mrmu/webpack-tutorial-1-examples

08/21 更新:追加整合 Bootstrap 4 beta

webpack 專用設定檔:webpack.config.js

上回我們簡單認識了 Webpack 的運作方式,現在我們要看更完整的 webpack 設定,所以要先認識 webpack.config.js。

本篇教學需要延續上次的教學檔案,請先確定你手邊有備好檔案,這樣比較容易上手。

上回我們是將 Webpack 的指令寫在 package.json 裡,但那樣一次只能打包一個 js 檔,看起來就不是一個好的設定方式,所以我們要使用 Webpack 專有的設定檔 webpack.config.js。現在在網頁專案目錄下新建一個 webpack.config.js,然後放進以下內容:

/* webpack.config.js : Webpack 的設定檔 */

var path = require('path');

module.exports = {
	entry: './src/js/main.js',
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: 'bundle.js'
	}
}

這個設定檔是使用 node.js 語法,簡單說明一下內容:
首先引入 path 這個 library 是為了要使用 path.resolve 這個函式去將 dist 目錄的絕對路徑取出來,再下一行是使用 module.exports 讓整份設定可以被外部取用。

設定內容裡有個 entry 屬性,entry 依字面翻譯就是一個進入點,代表 Webpack 應該從哪裡開始尋找整個專案的相依性關係,所以 Webpack 也不是你所想像那樣神奇,你還是必須給它一個起點,裡面要描述好要打包的檔案之間的關係 (用 module 的 import 和 export)。所以這裡要放入的是 main.js 的路徑。

Webpack 是可以設定多個 entry 的,這樣也會產生多個 bundle 檔案,有機會再來試試。

再來是 output 屬性,指定 path 屬性為「目標目錄」的絕對路徑;指定 filename 屬性為「目標目錄」下的檔案名稱,這裡不一定要叫 bundle.js,這只是個慣用檔名而已。

簡單說,我們現在就是要把 package.json 裡與 webpack 相關的設定移動出來,放在 webpack.config.js,那 package.json 裡也要做些修改:

{
  "name": "webpack-test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1",
    "build": "webpack",
    "build:prod": "webpack -p"
  },
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "webpack": "^3.5.1"
  }
}


可以看到 scripts 屬性裡的 build 屬性,指令改成 webpack 就可以了,build:prod 也是比照辦理,如此在執行 npm run build 或 npm run build:prod 時,就會自動使用 webpack.config.js 裡的設定了。現在你可以執行 npm run build,我們的 demo 站重整後應該是正常運作的。

$npm run build

所以 webpack.config.js 就是讓我們可以塞進很多打包設定的地方,現在開始把 css 也塞進去吧。

把 css 設定加進來 – 利用 Module Loaders

在 src/js/main.js 裡我們可以看見 import 的設定,比較直覺的想法就是在這裡也把 css import 進來,但 webpack 本身是看不懂css的,所以必須依賴其他 Loader 的協助。

如果你有使用過像 grunt 或 gulp 之類的任務工具,可以把 Loader 想像成也是排定任務的套件。Loader 可以設定規則來預先處理要載入的文件,比方像我們接下來要做的,你想載入 css 文件時,要預先處理它,讓 javascript 模組可以看得懂 css,接著才能載入使用。

所以我們來安裝 css-loader 和 style-loader 這兩個 loader 套件。

簡單講,css-loader 是單純作載入的套件,意即讓你寫的 css 可以載入到 js 裡。style-loader 則是作解析的動作,讓頁面能讀懂 css,所以它們倆合體就可以達成目的了。

於專案目錄下執行指令安裝 css-loader 和 style-loader:

$npm install css-loader style-loader --save-dev

完成後修改一下 src/js/main.js,在最上方加入兩個 import css 的設定:

/* src/js/main.js */

import '../css/style.css'; // 這裡
import '../css/buttons.css'; // 和這裡

import {myButton, myDesc} from './init';
myDesc.hide();
myButton.on('click', function(e){
    myDesc.toggle();    
});

webpack.config.js 也要加上新的 module 屬性設定:

/* webpack.config.js */

var path = require('path');

module.exports = {
	entry: './src/js/main.js',
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: 'bundle.js'
	},
	module: {
		rules: [
			{
				test: /.css$/, // 針對所有.css 的檔案作預處理,這邊是用 regular express 的格式
				use: [
					'style-loader',  // 這個會後執行 (順序很重要)
					'css-loader' // 這個會先執行
				]
			}
		]
	}
}

在 module 屬性裡,我們寫的規則大概是這樣的:當遇到要載入 .css 檔案時,先用 css-loader 載入,再用 style-loader 解析,這邊使用的順序很重要,因為 webpack 會反向使用它們。

現在我們已經在 main.js 用 include 的方式,把 css 檔載入了,這表示我們的 index.html 就不需要另外載入 css 檔了,修改一下 index.html:

<!-- index.html -->

    ...(略)...
    <!-- 載入自製的css: 因為都透過 main.js 載入了,所以都不用另外載入囉 -->
    <!--
    <link rel="stylesheet" href="src/css/style.css">
    <link rel="stylesheet" href="src/css/buttons.css">
    -->
    ...(略)...

執行 build:

$npm run build

你會發現 demo 站運作正常,到此你也成功打包 css 檔了。

Webpack 的完整流程

我們剛才提到的 Entry、Loaders 和 Output,都是 webpack.config.js 的一部份,再加上 Plugins 就會是一個完整的設定流程,如下:

EntryLoadersPluginsOutput

現在我們可以簡單的加上一個 Plugin 看看它是怎麼運作的,現在我們要加入 webpack 內建的 uglify js plugin,把打包後的 bundle.js 變得更小,首先打開 webpack.config.js 在上面 require webpack 這個函式庫,然後再加上一個新的 plugin 的設定:

/* webpack.config.js */

var path = require('path');
var webpack = require('webpack'); // 這裡

module.exports = {
	entry: './src/js/main.js',
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: 'bundle.js'
	},
	module: {
		rules: [
			{
				test: /.css$/, // 針對所有.css 的檔案作預處理,這邊是用 regular express 的格式
				use: [
					'style-loader',  // 這個會後執行 (順序很重要)
					'css-loader' // 這個會先執行
				]
			}
		]
	},
	plugins: [
		new webpack.optimize.UglifyJsPlugin(function(){
			// 這裡應該可以設定一些東西,但不是本篇想討論的,有興趣可以 Google 這個 plugin 可以做啥...
		}),
	]
}

現在 Webpack 會從 main.js 開始它的旅程,載入 css 時會先執行 module 裡的 rules,把 css 檔載入並解析完成,然後用 uglify js plugin 把整個 main.js 壓縮好,再 output 到目標目錄 dist 下的 bundle.js,就此完成一個完整的小打包流程了。

加入 SCSS

回到實用性上,雖然把 css 檔匯入 js 看起來潮潮der,但現在比較流行的作法,還是先寫成 scss 然後再轉成 css 檔,原因是這樣 js 的 build 速度會比較快,再來是 css 獨立檔案在網站載入時,可以跟 js 檔案平行載入,如果你的 css 檔案非常大,在載入速度上會有明顯的好處。

來看看怎麼做。

首先我們新增一個 scss 目錄,裡面放進 3 個 .scss 檔案,分別叫 style.scss、buttons.scss、_color.scss,現在我們的目錄結構長這樣:

專案目錄名稱/
│
├── index.html
├── package.json (npm init時自動產生)
├── webpack.config.js
├── node_modules/ (用npm安裝東西就會自動產生)
├── dist/ (目標目錄,執行 build 指令後會自動產生)
└── src/
      ├── css/ (我們這回合改用 scss 就不會用到這個目錄了)
      │     ├── style.css 
      │     └── buttons.css 
      ├── scss/
      │     ├── _color.css 
      │     ├── buttons.css 
      │     └── style.css 
      └── js/
             ├── init.js 
             └── main.js

來看看它們的內容:

首先是 _color.scss:

/* src/scss/_color.scss - 定義專案的主識別色和次識別色*/

$primary_color: #336699;
$secondary_color: #686868

_color.scss 它的檔名是以 _ 開頭,就是用來被匯入的css檔,裡面通常會是定義用於 scss 檔的變數,我們在這裡定義一些顏色。

再來看 buttons.scss:

/* src/scss/buttons.scss - 定義按鈕樣式,使用了_color.scss 的顏色定義 */

@import "colors";

button {
    border: 1px solid $primary_color;
    color: $primary_color;
    padding: 8px;
    font: inherit;
    cursor: pointer;
    outline: none;

    &:hover {
        background-color: $secondary_color;
        color: #fff;
    }
}

最後是 style.scss:

/* src/scss/style.scss - 也使用 _color.scss 的定義 */

@import "colors";

body {	
	font-family: Helvetica, Arial, sans-serif;
	text-align: center;
}
h1 {
	border: 1px solid $primary_color;
	color: $primary_color;
	padding: 5px;
}
p {
	border: 1px solid $secondary_color;
	padding: 20px;
	margin: 30px auto;
	width: 50%;
	font-size: 18px;
}

好的,算是寫了一些簡單的 scss 檔了,接下來我們要讓 js 看懂 scss 檔,所以我們又要用 NPM 裝一些套件了:

npm install sass-loader node-sass extract-text-webpack-plugin --save-dev

我們一共裝了三個套件,安裝 sass-loader 能讓 sass/scss 轉譯成 css,又由於相依性的關係,所以也一併安裝 node-sass
然後我們還需要 extract-text-webpack-plugin 這個 plugin 來幫我們把轉譯後的 css 另存成 css 檔。

Extract Text Webpack Plugin 本身有一個 extract 方法可以建立一個 Loader,把其他 Loader 轉換好的資源再包成可另存出來的模組設定 (這邊我沒深入了解,大概理解成這樣),到 Plugins 處理階段時,只要有 Exract Text Plugin 實例有指定好輸出的檔名,就可以順利存成 css 檔。所以它雖是 Plugin 但不只是用在 Plugins 屬性裡。

安裝完畢後,我們來調整一下 webpack.config.js 的設定:

/* webpack.config.js */

var path = require('path');
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin'); // 使用 extract text webpack plugin

// 建立一個 extract text plugin 的實例
var extractPlugin = new ExtractTextPlugin({
   filename: 'bundle.css' // scss轉css後另存的目標檔名
});

module.exports = {
	entry: './src/js/main.js',
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: 'bundle.js',
	},
	module: {
		rules: [
			{
				test: /.css$/,
				use: [
					'style-loader',  // 這個後 (順序很重要)
					'css-loader' // 這個先
				]
			},
        		{
 				test: /.scss$/,
				use: extractPlugin.extract({ //利用 extractPlugin 實例裡的 extract 來建立 Loader
					use: [
						'css-loader', 
						'sass-loader'
					]
				})
			}
		]
	},
	plugins: [
		new webpack.optimize.UglifyJsPlugin(function(){
			// ...
		}),
		extractPlugin // 把extract過的loader轉存成css檔 (我的理解啦XD)
	]
}

調整好 webpack.config.js 後,我們的 js 已經可以看得懂 scss 了,並且會自動把 scss 轉存成 css,所以也要調整一下 main.js,讓它載入我們有用到的 scss 檔:

/* src/js/main.js */

// import '../css/style.css'; //本回合未使用
// import '../css/buttons.css'; //本回合未使用
import '../scss/_colors.scss'; // 加入潮潮的scss
import '../scss/style.scss'; // 加入潮潮的scss
import '../scss/buttons.scss'; // 加入潮潮的scss

import {myButton, myDesc} from './init';
myDesc.hide();
myButton.on('click', function(e){
    myDesc.toggle();    
});

現在我們預期 build 時,main.js 會載入 scss,此時 webpack 會將 .scss 轉成 .css,再把結果用 extract 輸出到目標路徑 dist 下的 bundle.css。

所以我們的 index.html 也要加上載入 bundle.css 的描述:

<!-- index.html -->

    ...(略)...
    <!-- 載入自製的css -->
    <link rel="stylesheet" href="dist/css/bundle.css">
    ...(略)...

跑一下 npm run build:

$npm run build

會發現 dist/ 下多了一個 bundle.css,而 demo 站也運作正常。(運氣好的話啦)
如果你用的是 npm run build:prod,你的 bundle.css 也會被 minify。

追加介紹:Babel

Babel 是一個 JavaScript 的 Compiler,可以把你寫的一些比較先進高大上的 ES6 (ES 2015) JavaScript 語法編譯成舊版的相容語法,原因是主流瀏覽器可能還沒支援這些新的語法,為了滿足開發者的傲嬌性格,只好推出 Babel 這樣的工具,讓大家寫新語法也能被瀏覽器看懂。

寫到這裡我本來很興奮(?),因為我們在第一篇的 Webpack 教學裡看到 main.js 可以帥氣的 import 其他 JavaScript 檔案 (init.js),我查了一下發現這是 ES6 語法,那有了 Babel 我們不就可以 import 來 import 去,而瀏覽器也看得懂嗎?

答案是不行,import 會被 Babel 編譯成 require,但這種語法只能在 Server 端使用 (寫你的 node.js 吧,不然就用 Webpack!),Client 端(也就是瀏覽器) 不支援。

… 好吧,反正如果你有心要寫 ES6 或 ES7,你就必須安裝 Babel 啦,一樣到專案目錄下用 npm 指令來安裝:

npm install babel-core babel-loaderbabel-preset-es2015 --save-dev

webpack.config.js 也要改一下:

/* webpack.config.js */

var path = require('path');
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin'); // 使用 extract text webpack plugin

// 建立一個 extract text plugin 的實例
var extractPlugin = new ExtractTextPlugin({
   filename: 'bundle.css' // scss轉css後另存的目標檔名
});

module.exports = {
	entry: './src/js/main.js',
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: 'bundle.js',
	},
	module: {
		rules: [
			{
				test: /.css$/,
				use: [
					'style-loader',  // 這個後 (順序很重要)
					'css-loader' // 這個先
				]
			},
        		{
 				test: /.scss$/,
				use: extractPlugin.extract({ //利用 extractPlugin 實例裡的 extract 來建立 Loader
					use: [
						'css-loader', 
						'sass-loader'
					]
				})
			},
			{
				test: /.js$/,
				use: [{
					loader: 'babel-loader',
					options: {
						presets: ['es2015']
					}
				}]
			},
		]
	},
	plugins: [
		new webpack.optimize.UglifyJsPlugin(function(){
			// ...
		}),
		extractPlugin // 把extract過的loader轉存成css檔 (我的理解啦XD)
	]
}

現在針對 js 檔,也有預處理的 Loader 了,在輸出成 bundle.js 前,都會把高大上語法轉成瀏覽器看得懂的語法了。其實如果是 Chrome 的話對 ES6 的支援度已經非常高了,會需要 babel 主要還是想讓 IE 的使用者好過一點。

整合 jQuery

我們的 Demo 站已經在使用 jQuery 了,但你可以看到我們現在匯入 jQuery 的方式是直接寫在 index.html,載入遠端的 CDN 檔案。
現在開發者比較流行的方式是會把第三方套件都一併打包,可能是覺得放自己機器比放 CDN 妥當 (?),總之很多佈景版型其實也都使用這種打包方式,所以現在我們就要用 NPM 來安裝 jQuery 了:

$npm install --save jquery

裝好了就要來載入它,到 main.js 加上載入 jQuery 的語法:

/* src/js/main.js */

import 'jquery'; //這裡
import '../scss/_colors.scss';
import '../scss/style.scss';
import '../scss/buttons.scss';

import {myButton, myDesc} from './init';
myDesc.hide();
myButton.on('click', function(e){
    myDesc.toggle();    
});

最後,webpack.config.js 也需要增加一個 plugin 描述:

/* webpack.config.js */

var path = require('path');
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin'); // 使用 extract text webpack plugin

// 建立一個 extract text plugin 的實例
var extractPlugin = new ExtractTextPlugin({
   filename: 'bundle.css' // scss轉css後另存的目標檔名
});

module.exports = {
	entry: './src/js/main.js',
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: 'bundle.js',
	},
	module: {
		rules: [
			{
				test: /.css$/,
				use: [
					'style-loader',  // 這個後 (順序很重要)
					'css-loader' // 這個先
				]
			},
        		{
 				test: /.scss$/,
				use: extractPlugin.extract({ //利用 extractPlugin 實例裡的 extract 來建立 Loader
					use: [
						'css-loader', 
						'sass-loader'
					]
				})
			},
			{
				test: /.js$/,
				use: [{
					loader: 'babel-loader',
					options: {
						presets: ['es2015']
					}
				}]
			},
		]
	},
	plugins: [
		new webpack.optimize.UglifyJsPlugin(function(){
			// ...
		}),
		extractPlugin,
		new webpack.ProvidePlugin({ // 利用 webpack.ProvidePlugin 讓 $ 和 jQuery 可以連結到 jquery library
			$: 'jquery',
			jQuery: 'jquery'
		}),
	]
}

最後到 index.html,把原本載入遠端CDN的 jQuery 描述刪掉後存檔。
最後 build:

$npm run build

你會發現 Demo 站運作正常,但是 console 有噴錯,原因是 bootstrap 載入時,jQuery 還沒載入,因為 jQuery 已經被整合到 bundle.js 了。
所以,如果你要載入 bootstrap,應該要用 npm 把整包 bootstrap 載入再來設定 webpack,但現在 bootstrap 4 剛出 beta,然後我看了 bootstrap-loader 的設定還蠻麻煩的XD,所以這個等改天研究成功再來教學吧,不過我相信你看完教學後也有能力進一步研究的,所以也期待你的教學。:)

我把 bootstrap 4 beta 也加入了,請往下看。

加入 Bootstrap 4

發現把 bootstrap 4 beta 加入其實蠻簡單的,不用 bootstrap-loader 也行。首先使用 npm 安裝 popper.js 和 bootstrap 4.0.0-beta:

$npm install --save popper.js [email protected]

bootstrap 4.0.0 alpha 原本使用的是 Tether,到了 beta 後改用 popper.js,所以為了相依性需要安裝 popper.js。
再來是 webpack.config.js,babel loader 那邊的設定要排除 node_modules,不然會發生錯誤。再來是 plugins 的屬性要加上 popper.js 的設定,如下:

/* webpack.config.js */

var path = require('path');
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin'); // 使用 extract text webpack plugin

// 建立一個 extract text plugin 的實例
var extractPlugin = new ExtractTextPlugin({
   filename: 'bundle.css' // scss轉css後另存的目標檔名
});

module.exports = {
	entry: './src/js/main.js',
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: 'bundle.js',
	},
	module: {
		rules: [
			{
				test: /.css$/,
				use: [
					'style-loader',  // 這個後 (順序很重要)
					'css-loader' // 這個先
				]
			},
        		{
 				test: /.scss$/,
				use: extractPlugin.extract({ //利用 extractPlugin 實例裡的 extract 來建立 Loader
					use: [
						'css-loader', 
						'sass-loader'
					]
				})
			},
			{
				test: /.js$/,
				exclude: /(node_modules|bower_components)/, // 注意要排除 node_modules
				use: [{
					loader: 'babel-loader',
					options: {
						presets: ['es2015']
					}
				}]
			},
		]
	},
	plugins: [
		new webpack.optimize.UglifyJsPlugin(function(){
			// ...
		}),
		extractPlugin,
		new webpack.ProvidePlugin({ // 利用 webpack.ProvidePlugin 讓 $ 和 jQuery 可以連結到 jquery library
			$: 'jquery',
			jQuery: 'jquery'
			'window.jQuery': 'jquery',
			// Tether: 'tether', //4.0.0-alpha.6
			// tether: 'tether', //4.0.0-alpha.6
			Popper: ['popper.js', 'default'] //4.0.0-beta
		}),
	]
}

接著是 src/js/main.js 也要加上載入 bootstrap (js 和 scss) 的描述:

/* src/js/main.js */

import 'jquery';
import 'bootstrap'; // importing bootstrap.js

import 'bootstrap/scss/bootstrap.scss'; // bootstrap.scss
import '../scss/_colors.scss';
import '../scss/style.scss';
import '../scss/buttons.scss';

import {myButton, myDesc} from './init';
myDesc.hide();
myButton.on('click', function(e){
    myDesc.toggle();    
});

如此一來,bootstrap.js 會被加到 bundle.js,而 bootstrap.css 會被加到 bundle.css。

最後改一下 index.html,現在你的css設定只需要這行:

<link rel="stylesheet" href="dist/bundle.css"/>

而js的匯入只需要這行:

<script src="dist/bundle.js"></script>

標籤:, , , ,

分類:, ,

本文作者是Audi Lu

1 則留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

*

*

*

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料