0%

[筆記] 使用 Webpack 蓋出 Vue-CLI 的打包程序

這篇文章要來講如何用 webpack 蓋出一個 SPA 的前端打包程序。
這些事情在 Vue-CLI 裡面只要一個 serve/build 就搞定了,但是如果要自己用 webpack 做出類似的打包該怎麼做?

如果你對 webpack 不熟悉,可以到 webpack 教學文件 做到「起步」的步驟,大概就會有點概念了。

開發環境

先說明一下我的開發語言:

  • Vue
  • scss
  • HTML
  • JS6+

Vue-CLI 做了哪些打包?

來列一下 Vue-CLI 支援什麼:

  • 打包 .vue 檔
  • 可以在 .vue 檔中使用預處理語言 (scss/pug)
  • 用 babel 轉譯
  • CSS 自動前綴(墜) (autoprefix)
  • 壓縮圖片變成 base64 格式
  • 自動網頁重載 Hot Module Reload (HMR)
  • CSS + JS sourcemap

以上就是這篇文章要實現的功能。

設置 webpack 環境

首先當然要有 webpack,所以基本款是安裝 webpack webpack-cli,假裝專業一點加入 webpack-merge,可以用來切分 productiondevlopment 環境。

1
npm install --save-dev webpack webpack-cli webpack-merge

接著在根目錄建立 webpack 用的設定檔

  • webpack.common.js (共通設定)
  • webpack.prod.js (production 設定)
  • webpack.dev.js (devlopment 設定)
1
2
3
4
5
6
7
8
9
10
11
12
// webpack.common.js
const path = require('path');
module.exports = {
entry : {},
output: {
filename: '[name].bundle.js', // 根據 entry 的 key name 決定name
path: path.resolve(__dirname, 'build'),
},
module:{
rules: []
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// webpack.dev.js

const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, { // 合併 webpack.common.js 的設定
mode: 'development',
entry:{
app: ['./src/index.js'],
},
plugins:[],
output : {}
});
1
2
3
4
5
6
7
8
9
10
11
// webpack.prod.js

const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
entry:{
app: './src/index.js'
},
devtool : "source-map"
});

好,好長啊orz。
好,接下來設定好 npm script,我們就可以在 command line 快速打包。

1
2
3
4
5
6
// package.json
"scripts": {
"build": "webpack --config webpack.prod.js",
"watch": "webpack --watch --config webpack.dev.js"
// 這邊的 watch 是監看指令,當打包路徑上的檔案有變化時,會自動重新打包。但網頁不會自動刷新。
},

好,這麼一來,兩個環境的進入點都是 ./src/index.js,所以請新增一個 src 資料夾,底下開一個 index.js 做個樣子。

// 目前的目錄結構
build/
src/
  index.js
webpack.common.js
webpack.dev.js
webpack.prod.js

vue-loader 處理 .vue 檔

好,離上市集資已經跨了一大步了。
Webpack 簡單來說就是用一堆 loader 去處理依賴圖(dependencies graph)上的檔案。每種檔案都會有相對應的 loader,要有loader,webpack才知道要怎麼處理這個檔案。vue-loader 就是用來處理 .vue 檔的。

Vue Loader 簡體中字文件

首先來把以下東西裝進去:
npm install --save-dev vue-loader vue-template-compiler
npm install --save vue

vue-loadervue-template-compiler 就是拿來 load .vue 檔的。vue-loader會使用 vue-template-compiler 先對 .vue 檔做預處理。

注意一下 vue-template-compiler 要跟你的 vue 版本相同,否則會出錯。
安裝完後,可以到 package.json 去確認兩個的版本是不是一樣的。

好,接下來要設定 webpack,把 loader 裝上去。到 webpack.common.js 設定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

// ./webpack.common.js

const path = require('path');
// 新增
const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
// ...略

// 新增
plugins: [ new VueLoaderPlugin() ],

// ...

module: {
rules: [
// 新增 rules
{
test: /\.vue$/,
loader: 'vue-loader',
},
]
}
}

rules 的部分就是指定對 .vue 檔使用 vue-loader
VueLoaderPlugin 的作用,引用一下官網解釋

这个插件是必须的! 它的职责是将你定义过的其它规则复制并应用到 .vue 文件里相应语言的块。例如,如果你有一条匹配 /.js$/ 的规则,那么它会应用到 .vue 文件里的 script 块。

也就是說,你如果有對其他檔案或是語言使用 loader 去處理的話,VueLoaderPlugin會使在 .vue 檔中的相應語言也被處理。
所以,威~~什麼在 .vue 檔裡面的各種語言會被處理呢?就是因為有 VueLoaderPlugin把其他 loader 套進去的喔。

這麼一來,webpack就設定好惹。

接著我們來用.vue檔寫個 halloween 吧。在 src 目錄下開一個 App.vue,然後寫個內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

// ./src/App.vue

<template>
<div id="app">
<h1> {{ msg }} </h1>
</div>
</template>

<script>
export default {
data(){
return {
msg : "Halloween"
}
}
}
</script>

接著接著,在 src 目錄下,寫個 index.js:

1
2
3
4
5
6
7
8

// ./src/index.js
import Vue from 'vue';
import App from './App.vue'

new Vue({
render : h => h(App)
}).$mount("#app")

接著接著接著,
在根目錄的 build 目錄下開一個 app.html,在他的 body 裡面:

1
2
3

<div id="app"></div>
<script src="app.bundle.js"></script>

好了,準備完成,接著來打個包:

npm run build

然後你就會看到 app.bundle.js 降臨在 build 資料夾中。然後打開 app.html,你就會看到 halloween 的字。

這表示什麼?
表示我們打包成功了QWQ。

處理 css

接下來處理CSS。
套用官網的教學範例,需要 style-loadercss-loader。但是在 vue-loader 中,有個已經安裝好的 vue-style-loader,跟 style-loader的效果相同。所以我們只要裝 css-loader 就好:

npm install --save-dev css-loader

根據 vue-style-loader 的說法,這個 loader 跟style-loader不同,可以支援一些 SSR 的需求。有興趣的人可以參考 vue-style-loader

有 loader 之後呢?裝上去啊哈哈哈哈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ./webpack.common.js
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
},
{ // 以下新增
test: /\.css$/,
use : [
'vue-style-loader',
// 讓 JS 透過 style 標籤插入 CSS
{ // 把 CSS 轉成 CommonJS
loader: 'css-loader'
}

]
}
]

然後到 APP.vue 裡面加入一個 style

1
2
3
4
5
<style>
h1 {
color : red;
}
</style>

然後然後重新打包 npm run watch,打包完後,對網頁重新整理,就會發現 h1 變成紅色,他的樣式在 head 裡的 style 標籤裡面。

處理 scss

承接上面處理 css 的部分,處理 scss 還需要其他套件 node-sasssass-loader

npm install --save-dev node-sass sass-loader

大家都知道 sass/scss 是需要編譯器的,node-sass 就是一個用 C++ 寫出來的編譯器。而地方的 sass-loader 需要 node-sass 來滿足他編譯 scss/sass的需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ./webpack.common.js

rules: [
// .......前略
{
test: /\.scss$/, // 1. 改變匹配的檔案名稱
use : [
'vue-style-loader',

{
loader: 'css-loader'
},
// 2. 以下新增
{
loader: 'sass-loader', // compile scss/sass by node sass
options: {
// prependData,這裡的內容所有 scss/sass 都可以取用,通常可以放 variable/mixin 等,比如 $color 變數 ,可以在 App.vue中使用
// 你也可以从一个文件读取,例如 `variables.scss`
// 如果 sass-loader 版本 < 8,这里使用 `data` 字段
prependData: '$color: blue;',

}
}

]
}
]

注意上面的 1 跟 2 兩個步驟都做完。
然後改一下 App.vue 的 style 的部分:

1
2
3
4
5
<style lang="scss"> <!-- 注意: 要加個 lang="scss" -->
h1 {
color : $color; <!-- 注意: 使用 prependData 載入的 $color 變數,他是藍色的 -->
}
</style>

然後重新打包 npm run watch ,打包完,打開app.html,就發現字變成藍色的,表示 scss 打包成功啦哈哈哈哈哈。

自動前綴 postcss-loader + autoprefixer

打到這裡覺得好累,
相信你要是能看到這裡一定也覺得很累 orz

但是,接下來,要來加上自動前綴的功能惹~

npm install --save-dev postcss-loader autoprefixer

postcss 是一個 css 的後處理器,而 sass 是前處理器。
而 autoprefixer 是 postcss 的一個 plugin,顧名思義就是可以幫css自動加上前綴字。
同樣的,把他裝上去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ./webpack.common.js

rules: [
// .......前略
{
test: /\.scss$/,
use : [
'vue-style-loader',

{
loader: 'css-loader'
},
// 以下新增
{
loader: "postcss-loader",
options: {
ident: 'postcss', // 官方建議使用
plugins: [ // 插入 autoprefixer
require('autoprefixer')({}),
]
}
},
{
loader: 'sass-loader',
options: {
prependData: '$color: blue;',

}
}

]
}
]

接著來隨便寫一個應該需要 prefix 的 CSS 屬性

1
2
3
4
5
6
<style lang="scss"> 
h1 {
color : $color;
transform : translateX(50%);
}
</style>

然後就可以打包了~ 還沒啊啊啊
要加上前綴字,需要知道對象的瀏覽器版本,我們需要在 根目錄 加一個檔案 .browserslistrc,裡面加上對應的瀏覽器資訊。

> 1%
last 2 versions

其實這個設定也可以寫在 loader 上,只是其他套件(Babel)也會用到這個.browserslistrc 的資訊,所以把他獨立出來,整個專案對瀏覽器的支援才會統一。

加上去了吼?來,打個包。
打開網頁用開發者工具看,你會看到 transform 前面多一個有前綴字的屬性。
(這篇文章是在 2020/4 寫的,這時候會出現 -webkit-transform 的屬性)

postcss 還有其他有趣的功能,有興趣的人可以研究研究。

加上 CSS sourcemap

最後,來幫CSS 加上 sourcemap。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// ./webpack.common.js

rules: [
// .......前略
{
test: /\.scss$/,
use : [
'vue-style-loader',

{
loader: 'css-loader',
options: {
sourceMap: true, // 新增 1
}
},

{
loader: "postcss-loader",
options: {
ident: 'postcss',
plugins: [
require('autoprefixer')({}),
],
sourceMap: true, // 新增 2

}
},
{
loader: 'sass-loader',
options: {
prependData: '$color: blue;',
sourceMap: true, // 新增 3

}
}

]
}
]

css-loaderpostcss-loadersass-loader 的 options 都加上 sourceMap,然後設定成 true,就會生成 sourceMap 了。
如果這時候你重新打包,再開啟網頁,你就會看到樣式上面有標註來源。

根據CSS-loader webpack 官方文件說法:

They are not enabled by default because they expose a runtime overhead and increase in bundle size (JS source maps do not).

這個功能是預設不開啟的,因為會增加 bundle file 的大小,而且會讓打包速度變慢……
所以好像應該要把這兩個環境設定切開。

這邊說明一下,我想不到一個看起來很專業的切分法(比如說使用 webpack merge 去處理)

其實也可以這樣寫:

sourceMap : process.env.NODE_ENV !== "production"

其實這樣就解決了。

但是這個解法總給我一種:「既然這樣,為什麼要切分 dev / prod 兩個檔案?」的疑問。而且,如果我今天只從這兩個檔案的層級去看環境差異的話,我就可能會漏掉 sourceMap 的環節。

所以底下我想了一個不是那麼成熟的解法:
其實 merge 這東西就是在合併一般的 javascript Object 而已,所以可以像是修改物件一樣,在 prod/dev 兩個檔案中做出不同的修改:

首先,把 webpack.common 的選項包到一個 function 中,讓 sourceMap 變成一個參數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

module.exports = common;

function common (sourceMap){
sourceMap = Boolean(sourceMap);
return {
entry: {
},
// ......................
module: {
rules: [
// .......
{
test: /\.s[ac]ss$/,
use : [
'vue-style-loader',
{
loader: 'css-loader',
options: {
sourceMap: sourceMap,
// 1. 根據參數決定
},
},
{
loader: "postcss-loader",
options: {

ident: 'postcss',
plugins: [
require('autoprefixer')({}),
],
sourceMap: sourceMap,
// 2. 根據參數決定
}
},
{
loader: 'sass-loader',
options: {
prependData: '$color: blue;',
sourceMap: sourceMap,
// 3. 根據參數決定
}
}

]
}

]
},
};
}

接著分別設定dev/prod兩個檔案,對 common 函式傳入不同的參數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// webpack.dev.js
const merge = require('webpack-merge');
const webpack = require('webpack');
let common = require('./webpack.common.js');
// 1. 將 common 改成 let 變數;

// 2. 生成 common 物件,決定 sourceMap 的值。
const sourceMap = true;
common = common(sourceMap);

module.exports = merge(common, {
mode: 'development',
devtool: 'inline-source-map',
entry:{
app: ['./src/index.js']
},
plugins:[],
output : {}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.prod.js
const merge = require('webpack-merge');
let common = require('./webpack.common.js');
// 1. 將 common 改成 let 變數;

// 2. 生成 common 物件,決定 sourceMap 的值。
const sourceMap = true;
common = common(sourceMap);

module.exports = merge(common, {
entry:{
app: './src/index.js',
},
mode: 'production',

});

不知道還有沒有更好的解法?

加上 JS sourceMap

都說到 sourceMap ,也順便來加一下 JS 的 sourceMap 吧。
其實這個 Webpack 有透過 devtool 選項做支援,你可以選擇不同的 devtool 生成自己想要的 sourceMap 格式。
跟 CSS 不同的是,他似乎不會大量增加打包後檔案的容量。
官方建議兩個模式下都加入 JS sourceMap 。因為 production 跟 devlopment 兩個環境的需求不同,所以要使用不同的 sourceMap。

所以就分別對 prod 跟 dev 兩個做不同的設定囉:

1
2
3
4
5
6
// webpack.dev.js

module.exports = merge(common, {
mode: 'development',
devtool: 'inline-source-map', // 增加
// 以下略.....
1
2
3
4
5
6
// webpack.prod.js

module.exports = merge(common, {
mode: 'production',
devtool : "source-map", // 增加
// 以下略.....

好,接著來測試一下,到 ./src/index.js 裡面:

1
2
3
4
5
6
7
8
9
10

import Vue from 'vue';
import App from './App.vue'

console.log("Can you see who am I ?")
// 新增

new Vue({
render : h => h(App)
}).$mount("#app")

然後打包之後,打開開發者工具的 console,就會看到 console.log 的位置在 index.js 的某行。

簡單多了 QQ

babel-loader 轉譯 javascript

終於要進到 babel 了。(我都念 babel)

npm install --save-dev babel-loader @babel/core @babel/preset-env
首先是 @babel/core@babel/preset-env 這兩個 babel 主要功能套餐,接著是 babel-loader 用來對接 webpack。

我們把 loader 裝上去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// webpack.common.js
modules :{
rules : [
// ...略
// 以下新增
{
test: /\.js?$/,
loader: 'babel-loader',

exclude: file => ( // 排除 node_modules 中非 .vue 檔
/node_modules/.test(file) &&
!/\.vue\.js/.test(file)
),
options: {
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3"
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": "3"
}
]
]
}
},
]
}

說明一下:
options 的部分會稍後說明,如果你有自己偏好的 babel 設定可以跳過。
exclude 選項指定了 babel 不要處理的檔案,通常會不處理node_module的檔案,但是 vue-loader 官方給了個建議設定,如上方範例,可以確保 node_modules 中的 .vue 檔被轉譯。

至於 babel 會用到的 browser 設定,會直接套用根目錄的 .browserslistrc,嘿嘿嘿,前面把他獨立成一個檔案有道理,這邊不用重新寫一次。

來講一下 option 的部分:
主要要講的是 transform-runtime,他主要是 babel 用來解決轉譯後的 code 多處重複的問題,可以把轉譯後的 code 縮小。

[@babel/plugin-transform-runtime]https://babeljs.io/docs/en/babel-plugin-transform-runtime

如果你跟我一樣就是死都要用 polyfill ,那來裝一下吧:

npm install --save-dev @babel/plugin-transform-runtime

以及另外要在 production 環境使用的:

npm install --save @babel/runtime-corejs3 @babel/runtime

然後 loader 的部分直接使用上方的設定,就行了。

自動重載 Hot Module Reload (HMR)

效果是:你不用自己按重新整理! orz

根據 webpack 官方文件,基本的基本上有兩種做法,

  1. webpack-dev-server
  2. webpack-dev-middleware + webpack-hot-middleware (伺服器用)

webpack-dev-server

npm install --save-dev webpack-dev-server

然後到 webpack.dev.js

1
2
3
4
5
6
7
8
9
// webpack.dev.js
// .......
mode: 'development',
devtool: 'inline-source-map',
devServer: {
openPage: 'app.html', // 開啟時,打開 app.html
host: '192.168.0.106', // 對同網域開啟連線
contentBase: './build',
},

接著到 package.json 中設定指令:

1
2
3
4
"scripts": {
// .... 略
"start": "webpack-dev-server --open --config webpack.dev.js"
},

好,這麼一來只要在 command line 上執行 npm start 就會開啟頁面。並且在每次修改依賴圖上的檔案時,就會自動刷新網頁囉。

webpack-dev-middleware + webpack-hot-middleware (伺服器用)

如果你是用像是 express 的後端框架,這兩個東西就是拿來當 middleware 插入的。只有 dev-middleware 的話,只會在伺服器開啟時打包一次,但是再加上 hot-middleware 的話,就可以在依賴圖上的檔案變化時刷新網頁。

npm install --save-dev webpack-dev-middleware webpack-hot-middleware

接著在 webpack.dev.js 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const webpack = require('webpack') // 新增
// .... 略
mode: 'development',
devtool: 'inline-source-map',
// 以下新增
entry:{
app: ['./src/index.js','webpack-hot-middleware/client'],
},
plugins:[
new webpack.HotModuleReplacementPlugin()
],
output : {
publicPath: '/', // Make sure express server serve on http://localhost:3000
}

// .... 略

接著在你設定中間層檔案上加入 middleware,比如我是使用 express,加在最前面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// app.js
var app = express();

if (process.env.NODE_ENV !== "production"){
// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.dev.js');
const compiler = webpack(config);

app.use(webpackDevMiddleware(compiler, {
noInfo: true, publicPath: config.output.publicPath,
}));
app.use(require("webpack-hot-middleware")(compiler));
}
//...... 略

最後,啟動 server ,你就會看到 webpack 執行打包,並且在更新依賴圖上的檔案時,重新打包+刷新網頁。

壓縮圖片變成 base64 格式

這邊要用的是在官方文件也有的 file-loader & url-loader。實際上做到壓縮這件事的是 url-loader。而 file-loader 可以把依賴圖上出現的對應檔案通通拉到 build 目錄中。

npm install --save-dev url-loader file-loader

接著設定 loader,可以只設定 url-loader。( 實戰 Webpack 的 file-loader 和 url-loader - 《Chris 技術筆記》)

1
2
3
4
5
6
7
8
9
10
11
12
rules: [
{
test: /\.(woff|woff2|eot|ttf|otf|png|svg|jpg|gif)$/,
use: {
loader: 'url-loader',
options: {
limit: 1000, // 1000 bytes 以下壓縮
name: '[hash:7].[ext]',
}
}
},
// 以下略 ................

好,接著來測試一下,在 App.vue 中

1
2
3
4
5
6
<template>
<div id="app">
<h1> {{ msg }} </h1>
<div class="bg"></div> <!--新增-->
</div>
</template>
1
2
3
4
5
.bg{
height : 500px;
width : 500px;
background-image: url("./1kb圖片位置.png");
}

最後,重新打包,打開 app.html,看一下開發者工具,就會發現他被壓成 base64 了。

結語

哈哈哈哈,搞到最後好像在 webpack 教學啊~~

參考資料: