目前自己組建的一個團隊正在寫一份面試圖譜,將會在七月中旬開源。內容十分豐富,第一版會開源前端方面知識和程式設計師必備知識,後期會逐步寫入後端方面知識。因為工程所涉及內容太多(目前已經寫了一個半月),並且還需翻譯成英文,所以所需時間較長。有興趣的同學可以 Follow 我的 Github 得到最快的更新訊息。
該文使用的 Webpack 版本為 3.6.0,本文分兩部分。第一步是簡單的使用 webpack,第二部分通過一個真實專案來配置 webpack,沒有使用任何的 CLI,都是一步步配置直到完成生產程式碼的打包。這是本專案對應的倉庫,每個小節基本都對應了一次 commit。
這是本文的大綱,如果覺得有興趣你就可以往下看了
Webpack 到底是什麼
自從出現模組化以後,大家可以將原本一坨程式碼分離到個個模組中,但是由此引發了一個問題。每個 JS 檔案都需要從伺服器去拿,由此會導致載入速度變慢。Webpack 最主要的目的就是為了解決這個問題,將所有小檔案打包成一個或多個大檔案,官網的圖片很好的詮釋了這個事情,除此之外,Webpack 也是一個能讓你使用各種前端新技術的工具。
簡單使用
安裝
在命令列中依次輸入
mkdir webpack-demo
cd webpack-demo
// 建立 package.json,這裡會問一些問題,直接回車跳過就行
npm init
// 推薦這個安裝方式,當然你也安裝在全域性環境下
// 這種安裝方式會將 webpack 放入 devDependencies 依賴中
npm install --save-dev webpack
複製程式碼
然後按照下圖建立檔案
在以下檔案寫入程式碼
// sum.js
// 這個模組化寫法是 node 環境獨有的,瀏覽器原生不支援使用
module.exports = function(a, b) {
return a + b
}
// index.js
var sum = require('./sum')
console.log(sum(1, 2))
複製程式碼
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./build/bundle.js"></script>
</body>
</html>
複製程式碼
現在我們開始配置最簡單的 webpack,首先建立 webpack.config.js
檔案,然後寫入如下程式碼
// 自帶的庫
const path = require('path')
module.exports = {
entry: './app/index.js', // 入口檔案
output: {
path: path.resolve(__dirname, 'build'), // 必須使用絕對地址,輸出資料夾
filename: "bundle.js" // 打包後輸出檔案的檔名
}
}
複製程式碼
現在我們可以開始使用 webpack 了,在命令列中輸入
node_modules/.bin/webpack
複製程式碼
沒問題的話你應該可以看到類似的樣子
可以發現原本兩個 JS 檔案只有 100B,但是打包後卻增長到 2.66KB,這之中 webpack 肯定做了什麼事情,我們去 bundle.js
檔案中看看。
把程式碼簡化以後,核心思路是這樣的
var array = [(function () {
var sum = array[1]
console.log(sum(1, 2))
}),
(function (a,b) {
return a + b
})
]
array[0]() // -> 3
複製程式碼
因為 module.export
瀏覽器是不支援的,所以 webpack 將程式碼改成瀏覽器能識別的樣子。現在將 index.html
檔案在瀏覽器中開啟,應該也可以看到正確的 log。
我們之前是在資料夾中安裝的 webpack,每次要輸入 node_modules/.bin/webpack
過於繁瑣,可以在 package.json
如下修改
"scripts": {
"start": "webpack"
},
複製程式碼
然後再次執行 npm run start
,可以發現和之前的效果是相同的。簡單的使用到此為止,接下來我們來探索 webpack 更多的功能。
Loader
Loader 是 webpack 一個很強大功能,這個功能可以讓你使用很多新的技術。
Babel
Babel 可以讓你使用 ES2015/16/17 寫程式碼而不用顧忌瀏覽器的問題,Babel 可以幫你轉換程式碼。首先安裝必要的幾個 Babel 庫
npm i --save-dev babel-loader babel-core babel-preset-env
複製程式碼
先介紹下我們安裝的三個庫
- babel-loader 用於讓 webpack 知道如何執行 babel
- babel-core 可以看做編譯器,這個庫知道如何解析程式碼
- babel-preset-env 這個庫可以根據環境的不同轉換程式碼
接下來更改 webpack-config.js
中的程式碼
module.exports = {
// ......
module: {
rules: [
{
// js 檔案才使用 babel
test: /\.js$/,
// 使用哪個 loader
use: 'babel-loader',
// 不包括路徑
exclude: /node_modules/
}
]
}
}
複製程式碼
配置 Babel 有很多方式,這裡推薦使用 .babelrc 檔案管理。
// ..babelrc
{
"presets": ["babel-preset-env"]
}
複製程式碼
現在將之前 JS 的程式碼改成 ES6 的寫法
// sum.js
export default (a, b) => {
return a + b
}
// index.js
import sum from './sum'
console.log(sum(1, 2))
複製程式碼
執行 npm run start
,再觀察 bundle.js
中的程式碼,可以發現程式碼被轉換過了,並且同樣可以正常 輸出3。
當然 Babel 遠不止這些功能,有興趣的可以前往官網自己探索。
處理圖片
這一小節我們將使用 url-loader
和 file-loader
,這兩個庫不僅可以處理圖片,還有其他的功能,有興趣的可以自行學習。
先安裝庫
npm i --save-dev url-loader file-loader
複製程式碼
建立一個 images
資料夾,放入兩張圖片,並且在 app
資料夾下建立一個 js 檔案處理圖片
,目前的資料夾結構如圖
// addImage.js
let smallImg = document.createElement('img')
// 必須 require 進來
smallImg.src = require('../images/small.jpeg')
document.body.appendChild(smallImg)
let bigImg = document.createElement('img')
bigImg.src = require('../images/big.jpeg')
document.body.appendChild(bigImg)
複製程式碼
接下來修改 webpack.config.js
程式碼
module.exports = {
// ...
module: {
rules: [
// ...
{
// 圖片格式正則
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: [
{
loader: 'url-loader',
// 配置 url-loader 的可選項
options: {
// 限制 圖片大小 10000B,小於限制會將圖片轉換為 base64格式
limit: 10000,
// 超出限制,建立的檔案格式
// build/images/[圖片名].[hash].[圖片格式]
name: 'images/[name].[hash].[ext]'
}
}
]
}
]
}
}
複製程式碼
執行 npm run start
,打包成功如下圖
可以發現大的圖片被單獨提取了出來,小的圖片打包進了 bundle.js
中。
在瀏覽器中開啟 HTML 檔案,發現小圖確實顯示出來了,但是卻沒有看到大圖,開啟開發者工具欄,可以發現我們大圖的圖片路徑是有問題的,所以我們又要修改 webpack.config.js
程式碼了。
module.exports = {
entry: './app/index.js', // 入口檔案
output: {
path: path.resolve(__dirname, 'build'), // 必須使用絕對地址,輸出資料夾
filename: "bundle.js", // 打包後輸出檔案的檔名
publicPath: 'build/' // 知道如何尋找資源
}
// ...
}
複製程式碼
最後執行下 npm run start
,編譯成功了,再次重新整理下頁面,可以發現這次大圖被正確的顯示了。下一小節我們將介紹如何處理 CSS 檔案。
處理 CSS 檔案
新增 styles
資料夾,新增 addImage.css
檔案,然後在該檔案中新增程式碼
img {
border: 5px black solid;
}
.test {border: 5px black solid;}
複製程式碼
這一小節我們先使用 css-loader
和 style-loader
庫。前者可以讓 CSS 檔案也支援 impost
,並且會解析 CSS 檔案,後者可以將解析出來的 CSS 通過標籤的形式插入到 HTML 中,所以後面依賴前者。
npm i --save-dev css-loader style-loader
複製程式碼
首先修改 addImage.js
檔案
import '../styles/addImage.css'
let smallImg = document.createElement('img')
smallImg.src = require('../images/small.jpeg')
document.body.appendChild(smallImg)
// let bigImg = document.createElement('img')
// bigImg.src = require('../images/big.jpeg')
// document.body.appendChild(bigImg)
複製程式碼
然後修改 webpack.config.js
程式碼
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader',
{
loader: 'css-loader',
options: {
modules: true
}
}
]
},
]
}
}
複製程式碼
執行下 npm run start
,然後重新整理頁面,可以發現圖片被正確的加上了邊框,現在我們來看一下 HTML 的檔案結構
從上圖可以看到,我們在 addImage.css
檔案中寫的程式碼被加入到了 style
標籤中,並且因為我們開啟了 CSS 模組化的選項,所以 .test
被轉成了唯一的雜湊值,這樣就解決了 CSS 的變數名重複問題。
但是將 CSS 程式碼整合進 JS 檔案也是有弊端的,大量的 CSS 程式碼會造成 JS 檔案的大小變大,操作 DOM 也會造成效能上的問題,所以接下來我們將使用 extract-text-webpack-plugin
外掛將 CSS 檔案打包為一個單獨檔案
首先安裝 npm i --save-dev extract-text-webpack-plugin
然後修改 webpack.config.js
程式碼
const ExtractTextPlugin = require("extract-text-webpack-plugin")
module.exports = {
// ....
module: {
rules: [
{
test: /\.css$/,
// 寫法和之前基本一致
loader: ExtractTextPlugin.extract({
// 必須這樣寫,否則會報錯
fallback: 'style-loader',
use: [{
loader: 'css-loader',
options: {
modules: true
}
}]
})
]
}
]
},
// 外掛列表
plugins: [
// 輸出的檔案路徑
new ExtractTextPlugin("css/[name].[hash].css")
]
}
複製程式碼
執行下 npm run start
,可以發現 CSS 檔案被單獨打包出來了
但是這時候重新整理頁面會發現圖片的邊框消失了,那是因為我們的 HTML 檔案沒有引用新的 CSS 檔案,所以這裡需要我們手動引入下,在下面的章節我們會通過外掛的方式自動引入新的檔案。
接下來,會用一個專案來繼續我們的 webpack 學習,在這之前,先 clone 一下專案。該專案原地址是 這裡,因為使用的 webpack 版本太低,並且依賴的庫也有點問題,故我將專案拷貝了過來並修改了幾個庫的版本號。
請依次按照以下程式碼操作
git clone https://github.com/KieSun/webpack-demo.git
cd webpack-demo
// 切換到 0.1 標籤上並建立一個新分支
git checkout -b demo 0.1
cd project
npm i
// 檢視分支是否為 demo,沒問題的話就可以進行下一步
複製程式碼
如何在專案中使用 webpack
專案中已經配置了很簡單的 babel 和 webpack,直接執行 npm run build
即可
這時候你會發現這個 bundle.js 居然有這麼大,這肯定是不能接受的,所以接下來章節的主要目的就是將單個檔案拆分為多個檔案,優化專案。
分離程式碼
先讓我們考慮下快取機制。對於程式碼中依賴的庫很少會去主動升級版本,但是我們自己的程式碼卻每時每刻都在變更,所以我們可以考慮將依賴的庫和自己的程式碼分割開來,這樣使用者在下一次使用應用時就可以儘量避免重複下載沒有變更的程式碼,那麼既然要將依賴程式碼提取出來,我們需要變更下入口和出口的部分程式碼。
// 這是 packet.json 中 dependencies 下的
const VENOR = ["faker",
"lodash",
"react",
"react-dom",
"react-input-range",
"react-redux",
"redux",
"redux-form",
"redux-thunk"
]
module.exports = {
// 之前我們都是使用了單檔案入口
// entry 同時也支援多檔案入口,現在我們有兩個入口
// 一個是我們自己的程式碼,一個是依賴庫的程式碼
entry: {
// bundle 和 vendor 都是自己隨便取名的,會對映到 [name] 中
bundle: './src/index.js',
vendor: VENOR
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
// ...
}
複製程式碼
現在我們 build 一下,看看是否有驚喜出現
真的有驚喜。。為什麼 bundle 檔案大小壓根沒變。這是因為 bundle 中也引入了依賴庫的程式碼,剛才的步驟並沒有抽取 bundle 中引入的程式碼,接下來讓我們學習如何將共同的程式碼抽取出來。
抽取共同程式碼
在這小節我們使用 webpack 自帶的外掛 CommonsChunkPlugin
。
module.exports = {
//...
output: {
path: path.join(__dirname, 'dist'),
// 既然我們希望快取生效,就應該每次在更改程式碼以後修改檔名
// [chunkhash]會自動根據檔案是否更改而更換雜湊
filename: '[name].[chunkhash].js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
// vendor 的意義和之前相同
// manifest檔案是將每次打包都會更改的東西單獨提取出來,保證沒有更改的程式碼無需重新打包,這樣可以加快打包速度
names: ['vendor', 'manifest'],
// 配合 manifest 檔案使用
minChunks: Infinity
})
]
};
複製程式碼
當我們重新 build 以後,會發現 bundle 檔案很明顯的減小了體積
但是我們使用雜湊來保證快取的同時會發現每次 build 都會生成不一樣的檔案,這時候我們引入另一個外掛來幫助我們刪除不需要的檔案。
npm install --save-dev clean-webpack-plugin
複製程式碼
然後修改配置檔案
module.exports = {
//...
plugins: [
// 只刪除 dist 資料夾下的 bundle 和 manifest 檔案
new CleanWebpackPlugin(['dist/bundle.*.js','dist/manifest.*.js'], {
// 列印 log
verbose: true,
// 刪除檔案
dry: false
}),
]
};
複製程式碼
然後 build 的時候會發現以上檔案被刪除了。
因為我們現在將檔案已經打包成三個 JS 了,以後也許會更多,每次新增 JS 檔案我們都需要手動在 HTML 中新增標籤,現在我們可以通過一個外掛來自動完成這個功能。
npm install html-webpack-plugin --save-dev
複製程式碼
然後修改配置檔案
module.exports = {
//...
plugins: [
// 我們這裡將之前的 HTML 檔案當做模板
// 注意在之前 HTML 檔案中請務必刪除之前引入的 JS 檔案
new HtmlWebpackPlugin({
template: 'index.html'
})
]
};
複製程式碼
執行 build 操作會發現同時生成了 HTML 檔案,並且已經自動引入了 JS 檔案
按需載入程式碼
在這一小節我們將學習如何按需載入程式碼,在這之前的 vendor 入口我發現忘記加入 router 這個庫了,大家可以加入這個庫並且重新 build 下,會發現 bundle 只有不到 300KB 了。
現在我們的 bundle 檔案包含了我們全部的自己程式碼。但是當使用者訪問我們的首頁時,其實我們根本無需讓使用者載入除了首頁以外的程式碼,這個優化我們可以通過路由的非同步載入來完成。
現在修改 src/router.js
// 注意在最新版的 V4路由版本中,更改了按需載入的方式,如果安裝了 V4版,可以自行前往官網學習
import React from 'react';
import { Router, Route, IndexRoute, hashHistory } from 'react-router';
import Home from './components/Home';
import ArtistMain from './components/artists/ArtistMain';
const rootRoute = {
component: Home,
path: '/',
indexRoute: { component: ArtistMain },
childRoutes: [
{
path: 'artists/new',
getComponent(location, cb) {
System.import('./components/artists/ArtistCreate')
.then(module => cb(null, module.default))
}
},
{
path: 'artists/:id/edit',
getComponent(location, cb) {
System.import('./components/artists/ArtistEdit')
.then(module => cb(null, module.default))
}
},
{
path: 'artists/:id',
getComponent(location, cb) {
System.import('./components/artists/ArtistDetail')
.then(module => cb(null, module.default))
}
}
]
}
const Routes = () => {
return (
<Router history={hashHistory} routes={rootRoute} />
);
};
export default Routes;
複製程式碼
然後執行 build 命令,可以發現我們的 bundle 檔案又瘦身了,並且新增了幾個檔案
將 HTML 檔案在瀏覽器中開啟,當點選路由跳轉時,可以在開發者工具中的 Network 一欄中看到載入了一個 JS 檔案。
首頁
點選右上角 Random Artist 以後
自動重新整理
每次更新程式碼都需要執行依次 build,並且還要等上一會很麻煩,這一小節介紹如何使用自動重新整理的功能。
首先安裝外掛
npm i --save-dev webpack-dev-server
複製程式碼
然後修改 packet.json 檔案
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server --open"
},
複製程式碼
現在直接執行 npm run dev
可以發現瀏覽器自動開啟了一個空的頁面,並且在命令列中也多了新的輸出
等待編譯完成以後,修改 JS 或者 CSS 檔案,可以發現 webpack 自動幫我們完成了編譯,並且只更新了需要更新的程式碼
但是每次重新重新整理頁面對於 debug 來說很不友好,這時候就需要用到模組熱替換了。但是因為專案中使用了 React,並且 Vue 或者其他框架都有自己的一套 hot-loader,所以這裡就略過了,有興趣的可以自己學習下。
生成生產環境程式碼
現在我們可以將之前所學和一些新加的外掛整合在一起,build 生產環境程式碼。
npm i --save-dev url-loader optimize-css-assets-webpack-plugin file-loader extract-text-webpack-plugin
複製程式碼
修改 webpack 配置
var webpack = require('webpack');
var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin')
var CleanWebpackPlugin = require('clean-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const VENOR = ["faker",
"lodash",
"react",
"react-dom",
"react-input-range",
"react-redux",
"redux",
"redux-form",
"redux-thunk",
"react-router"
]
module.exports = {
entry: {
bundle: './src/index.js',
vendor: VENOR
},
// 如果想修改 webpack-dev-server 配置,在這個物件裡面修改
devServer: {
port: 8081
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].[chunkhash].js'
},
module: {
rules: [{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: [{
loader: 'url-loader',
options: {
limit: 10000,
name: 'images/[name].[hash:7].[ext]'
}
}]
},
{
test: /\.css$/,
loader: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [{
// 這邊其實還可以使用 postcss 先處理下 CSS 程式碼
loader: 'css-loader'
}]
})
},
]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['vendor', 'manifest'],
minChunks: Infinity
}),
new CleanWebpackPlugin(['dist/*.js'], {
verbose: true,
dry: false
}),
new HtmlWebpackPlugin({
template: 'index.html'
}),
// 生成全域性變數
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("process.env.NODE_ENV")
}),
// 分離 CSS 程式碼
new ExtractTextPlugin("css/[name].[contenthash].css"),
// 壓縮提取出的 CSS,並解決ExtractTextPlugin分離出的 JS 重複問題
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
}),
// 壓縮 JS 程式碼
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
]
};
複製程式碼
修改 packet.json 檔案
"scripts": {
"build": "NODE_ENV=production webpack -p",
"dev": "webpack-dev-server --open"
}
複製程式碼
執行 npm run build
可以看到我們在經歷了這麼多步以後,將 bundle 縮小到了只有 27.1KB,像 vendor 這種常用的庫我們一般可以使用 CDN 的方式外鏈進來。
補充
webpack 配置上有些實用的小點在上文沒有提到,統一在這裡提一下。
module.exports = {
resolve: {
// 副檔名,寫明以後就不需要每個檔案寫字尾
extensions: ['.js', '.css', '.json'],
// 路徑別名,比如這裡可以使用 css 指向 static/css 路徑
alias: {
'@': resolve('src'),
'css': resolve('static/css')
}
},
// 生成 source-map,用於打斷點,這裡有好幾個選項
devtool: '#cheap-module-eval-source-map',
}
複製程式碼
後記
如果你是跟著本文一個個步驟敲下來的,那麼大部分的 webpack 配置你應該都是可以看懂了,並且自己應該也知道如何去配置。謝謝大家看到這裡,這是本專案對應的倉庫,每個小節基本都對應了一次 commit。
文章較長,有錯誤也難免,如果你發現了任何問題或者我有任何表述的不明白的地方,都可以留言給我。