文章同步發表在:部落格地址
為什麼要用webpack
關於為什麼要使用webpack,我比較認同的一種說法是:
webpack可以很好地管理你開發中遇到的各種HTML、JS、CSS以及各種圖片資原始檔,同時對應不同的資源,webpack還提供了對應的Loaders將其進行轉化為適用於瀏覽器使用的格式
如何從0開始上手webpack
後會無期裡面,阿呂說過這麼一句話:"你連世界都沒有觀過,哪裡來的世界觀",在我們實際的專案開發中,比如你用的是vue,那麼vue已經有很好的腳手架工具(vue-cli)供你使用了,或者有的開發團隊,會有技術Leader專門預先做好相關的模板,方便後來新加入的成員儘快上手專案,但是隨著你開發的專案越多,你可能會越不注意到那些最基本的東西,比如說這個模板到底是如何搭建的,或者說讓你自己來搭建一個模板給其他人用,是否也能做到如此的簡單易上手,我想這是每一個想要在前端路上進階的人需要考慮的問題。
最好的方式就是自己試著去搭一個最簡單的模板,從0到1的過程是最痛苦的,但是1到2或者說2到3都是水到渠成的事,下面就看看怎麼開始從0搭建一個基於webpack的vue的開發環境
建立專案
注:使用的webpack版本為^3.10.0 首先通過webstorm或者你手上的其他IDE建立一個空的專案,我這裡暫且叫proWebpack,建立好後是這個樣子的:
因為我們通過npm來管理包,然後每個專案的根目錄下都會有一個package.json
檔案來管理專案的配置資訊,包括名稱、版本、許可證等後設資料以及記錄所需的各種模組,包括 執行依賴,和開發依賴,我們可以通過在命令控制檯用npm init
命令建立一個package.json
檔案,但是這樣很麻煩,因為這樣npm
會通過命令列問答的方式來初始化並建立package.json
檔案,為了方便起見我們使用npm init -y
,這樣npm
就會使用一些預設值進行初始化:
ok現在我們得到一個package.json檔案,包含了一些簡單的資訊比如name、version等欄位,接下來,因為我們是想搭建一個用於vue開發環境的webpack目錄結構,到目前為止還沒看到vue的影子,依然是在命令列使用npm install vue --save
,這裡注意package.json
中的依賴包有dependencies
和devDependencies
兩種,這兩種的區別:dependencies
表示我們要在生產環境下使用該依賴,devDependencies
則表示我們僅在開發環境使用該依賴,我們install時候使用--save會把包安裝在dependencies 下面,所以執行完上面的命令你可以看到這樣的目錄結構:
{
"name": "proWebpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"vue": "^2.5.16"
}
}
複製程式碼
然後在devDependencies
下面安裝webpack,再次提醒,由於webpack4變化較大,這裡使用^3.10.0版本的webpack: npm install -D webpack@3.10.0
,此時目錄結構如下:
{
"name": "proWebpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"vue": "^2.5.16"
},
"devDependencies": {
"webpack": "^3.10.0"
}
}
複製程式碼
建立入口檔案
接下來建立入口檔案,在根目錄下建立一個index.html作為啟動頁面,一個webpack.config.js作為webpack配置檔案(實際專案中這裡會有webpack配置檔案,分別用於開發環境和生產環境,這裡簡便起見就用一個配置檔案),新建一個src目錄,在該目錄下新建一個index.js作為打包入口檔案:
proWebpack
├─ index.html 啟動頁面
├─ package-lock.json
├─ package.json 包管理
├─ src
│ └─ index.js 入口檔案
└─ webpack.config.js webpack配置檔案
複製程式碼
為了更接近vue-cli建立出來的模板,我們還需要建立一個Vue例項提供給入口檔案的el掛載,這個Vue檔案很簡單長這樣:
<template>
<div>proWebpack</div>
</template>
複製程式碼
然後寫好入口檔案:
import Vue from 'vue'
import App from '../App.vue'
new Vue({
el: '#app',
render: h => h(App)
})
複製程式碼
同時別忘記在Index.html裡面要提供一個供vue例項掛載的HTMLElement例項:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>proWebpack</title>
</head>
<body>
<div class="app">proWebpack</div>
</body>
</html>
複製程式碼
這樣準備工作做得差不多了,下面做最核心的部分
webpack.config.js的編寫
webpack有這麼幾個核心的概念:
- entry 入口起點,webpack會找出入口起點的直接或間接依賴項,將他們進行處理輸出為我們稱之為bundle的檔案中
- output 輸出路徑,告訴 webpack 在哪裡輸出它所建立的 bundles以及如何給這些buldles命名
- loader 因為webpack本身只能理解javascript檔案,所以當我們要用vue或者React的時候可以使用相關的loader(比如我們熟知的vue-loader)來將其轉換成webpack 能夠處理的有效模組
- plugins 擴充套件外掛,可以用於打包的優化和壓縮,這個可能要結合實際使用更好理解
- module 模組 webpack中的一切你都可以理解為模組
- chunk 程式碼塊,一個 chunk 由多個模組組合而成,用於程式碼合併與分割。
下面開始配置,因為所有輸出檔案的目標路徑必須是絕對路徑,所以這裡要用到使Node.js 的 path 模組
// 引入webpack和path模組
const path = require('path')
const webpck = require('webpack')
複製程式碼
簡單配置檔案的entry和output
const path = require('path')
const webpck = require('webpack')
const productionPath = require('./package.json').name
module.exports = {
entry: {
index: './src/index.js'
},
output: {
path: path.join(__dirname, 'dist', productionPath),
filename: 'bundle.js'
}
}
複製程式碼
這裡程式碼應該不難理解,就是從index.js
這個入口進去,把直接或間接依賴項打包輸出dist資料夾目錄下,
怎麼執行呢,我們需要在package.json
的scripts物件裡面新增我們自己的build命令:
// 這裡用最簡單的命令
"build": "webpack -w",
複製程式碼
因為我們的webpack配置檔案就叫webpack.config.js,預設情況下,webpack在執行的時候會搜尋當前目錄webpack.config.js 檔案執行,實際開發中我們會使用 --config 選項來指定配置檔案來對開發環境和生產環境的配置做出區分
直接在控制檯npm run build
,果不其然報錯了:
ERROR in ./App.vue
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type.
| <template>
| <div>proWebpack</div>
| </template>
@ ./src/index.js 2:0-28
複製程式碼
很明顯,webpack告訴我們他沒法識別vue檔案,需要我們提供一個相關的loader來處理成webpack可以識別的檔案,這裡先npm install vue-loader -D
,然後回到webpack.config.json
配置loader:
const path = require('path')
const webpck = require('webpack')
module.exports = {
entry: {
index: './src/index.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
// 模組配置
rules: [
//模組規則(配置 loader、解析器等選項)
{
// 這裡是匹配條件,每個選項都接收一個正規表示式或字串
// test是必須匹配選項
// exclude 是必不匹配選項(優先於 test 和 include)
// 對選中後的檔案通過 use 配置項來應用 Loader,可以只應用一個 Loader 或者按照從後往前的順序應用一組 Loader,同時還可以分別給 Loader 傳入引數。
test: /\.vue$/,
exclude: /node_modules/,
use: {loader: "vue-loader"}
}
]
}
}
複製程式碼
配置好之後再run一遍,咦,又報錯,我們看下主要的報錯資訊:
ERROR in ./App.vue
Module build failed: Error: Cannot find module 'vue-template-compiler'
at Function.Module._resolveFilename (module.js:527:15)
at Function.Module._load (module.js:476:23)
複製程式碼
cannot fount那就是需要我們去install唄,但是為什麼要下這個模組,我找到一個比較靠譜的說法:
其中 vue-template-compiler 是 vue-loader 的 peerDependencies,npm3 不會自動安裝 peerDependencies,然而 vue-template-compiler 又是必備的,那為什麼作者不將其放到 dependencies 中呢?有人在 github 上提過這個問題,我大致翻譯一下作者的回答(僅供參考):這樣做的原因是因為沒有可靠的方式來固定巢狀依賴的關係,怎麼理解這句話?首先 vue-template-compiler 和 vue 的版本號是一致的(目前是同步更新),將 vue-template-compiler 指定為 vue-loader 的 dependencies 並不能保證 vue-template-compiler 和 vue 的版本號是相同的,讓使用者自己指定版本才能保證這一點。檢視作者的回答(英文) 。如果兩者版本不一致,執行時就不會錯誤提示。
那隻需要我們npm i vue-template-compiler -D
一下,然後在跑一遍build指令碼,不出意外你應該能在專案根目錄看到一個dist資料夾,把這個資料夾裡面的js檔案引入啟動頁面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>proWebpack</title>
</head>
<body>
<div class="app">proWebpack</div>
<script src="/dist/bundle.js"></script>
</body>
</html>
複製程式碼
接下來我們要訪問這個頁面,這裡用到webpack提供的devsever來除錯我們的頁面, DevServer 會啟動一個 HTTP 伺服器用於服務網頁請求,同時會幫助啟動 Webpack ,並接收 Webpack 發出的檔案更變訊號,通過 WebSocket 協議自動重新整理網頁做到實時預覽。這就是為什麼你用vue-cli搭建出來的腳手架可以做到頁面實時渲染,配置這個也很簡單,在webpack.config.js
裡面新增如下配置:
devServer: {
port: 3000, // 埠號
}
複製程式碼
其實webpack-dev-server可以配置很多引數,這裡不過多展開,接下來需要修改下我們的build指令碼:
"build": "webpack-dev-server"
複製程式碼
別忘了npm install
一下webpack-dev-server
,最後執行npm run build看到控制檯報出成功資訊並告訴你你的專案正執行在localhost:3000:
$ npm run build
> proWebpack@1.0.0 build D:\proWebpack
> webpack-dev-server --open
Project is running at http://localhost:3000/
webpack output is served from /
複製程式碼
訪問localhost:3000看到如下顯示的話,恭喜你,webpack配置如何從0到1你應該已經清楚了:
更進一步
我們知道了怎麼初步配置webpack還遠遠不夠,實際開發中我們會遇到更多樣的情況,比如說 當我們只有一個輸出檔案的時候我們可以在output寫死,就像我們上面寫的:
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
}
複製程式碼
但是當我們有多個檔案輸出的時候,一個個去寫是一個費力不討好的做法,這個時候就要用到webpack內建的變數了,我們可以這麼寫:
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].[hash].js'
}
複製程式碼
這樣會賦予每一個bundle唯一的名稱,並且在每次構建過程中,生成唯一的 hash ,這時候稍微改動一下指令碼檔案:
"scripts": {
"start": "webpack-dev-server",
"build": "webpack -w",
"test": "echo \"Error: no test specified\" && exit 1"
},
複製程式碼
之前寫一起是為了方便理解, 現在我把跑本地伺服器和打包的指令碼拆開寫,更語義化,一般正常開發也是這麼做的,先跑一下build,你會發現此時dist資料夾下面生成了新的打包檔案index.8ad46f4fb5c8385db614.js
,然後把這個檔案同樣引入index.html
,跑start指令碼發現結果也是照常輸出,但是又有了新的問題,如果我們每次build之後都要手動引入帶著一長串hash的js檔案也是很蠢的事情,所以這裡我們可以用上一個plugin叫做html-webpack-plugin
,這個外掛主要有兩個作用:
1、在每次編譯完成之後動態為html檔案引入外部資源(script、link),對於我me你這種帶hash的檔名來說無疑是極為方便的
2、可以制定一個模板的html檔案,html-webpack-plugin可以根據這個模板來生成html入口檔案
下面看下如何使用:
// 引入
const HtmlWebpackPlugin = require('html-webpack-plugin')
//省略若干程式碼
plugins: [
new HtmlWebpackPlugin({
filename: './index.html', // 生成的入口檔案的名字,預設就是index.html
template: './index.tpl.html',// 有時候,外掛自動生成的html檔案,並不是我們需要結構,我們需要給它指定一個模板,讓外掛根據我們給的模板生成html
inject: 'body',// 有四個選項值 true, body, head, false----true:預設值,script標籤位於html檔案的 body 底部 body:同true head:插入的js檔案位於head標籤內 false:不插入生成的js檔案,只生成一個純html
})
],
複製程式碼
用到的index.tpl.html
只是和之前的index.html
作了一點小小的區分,讓人知道這是模板生成的:
// index.tpl.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>這是模板生成的</title>
</head>
<body>
<div class="app">proWebpack</div>
</body>
</html>
複製程式碼
方便起見我們在package.json
在新增一個clean的指令碼,每次build之前先刪除之前生成的dist檔案:
"scripts": {
"start": "webpack-dev-server",
"build": "npm run clean && webpack -w",
"clean": "rimraf dist", // 新增clean指令碼
"test": "echo \"Error: no test specified\" && exit 1"
}
複製程式碼
然後run build一下:
我們發現這個時候的入口檔案已經是根據模板自動生成的了,而且自動把生成的js檔案新增到了入口檔案裡面,同時npm run start命令看到的介面也是使用模板生成的頁面,這就大功告成了。最後
其實這裡還有一點沒有提到的就是css的處理,除了css-loader我們平常開發還會用到一些css前處理器如scss,還有postcss等樣式處理工具,這裡的處理其實也不難,感興趣的朋友可以自己嘗試一下,這裡由於篇幅原因(我想偷懶)就不過多展開了。