顯微鏡下的webpack4入門
前端的構建打包工具很多,比如grunt,gulp。相信這兩者大家應該是耳熟能詳的,上手相對簡單,而且所需手敲的程式碼都是比較簡單的。然後webpack的出現,讓這兩者打包工具都有點失寵了。webpack比起前兩者打包工具,對於前端程式設計師JS程式設計能力的要求還是挺高的。不過需要相容ie8及以下的小夥伴們,就不要考慮webpack了,他很傲嬌地不相容!
webpack 前期準備
webpack,這是一個組合詞“web”+“pack”,web就是網站的意思,“pack”有打包的意思,webpack組合在一起就是網站打包的意思,這個名字相當暴力簡單明瞭啊。webpack這款工具雖然很難學,但是自由度很大,玩轉之後有種隨心所欲的感覺。
在學習webpack之前,有幾個基礎的概念:
- JavaScript,如果這個程式設計能力不過關,比如不清楚ES6的語法,那麼webpack學起來有些費力,還是要先去學習基礎知識。
- nodejs,關於nodejs的日常用法,還是需要了解的,不然webpack改如何啟動,都無從下手。
- CommonJS,這個規範是需要學習下的,webpack的配置檔案就是按照這個規則。
- 如果以上幾個技能都具備,那麼恭喜我們可以開始webpack的學(求)習(虐)之旅了。
webpack 打包原理
在使用webpack之前,我們需要了解webpack的工作原理。webpack打包出來的JS不僅僅是壓縮混淆我們的原始檔,而且還對它做了其他的處理。
下面是webpack打包出來的JS檔案和原始檔:
"./src/index.js"
原始檔
let str="index"
console.log(str)
- webpack打包後
(function(modules) { // webpackBootstrap /*此處省略N+1行*/ return __webpack_require__(__webpack_require__.s = "./src/index.js"); }) ({ "./src/index.js":(function(module, exports) { eval("let str=\"index\"\r\nconsole.log(str)\n\n//# sourceURL=webpack:///./src/index.js?"); }) });
是不是感覺本來小巧的JS,一下子變得臃腫了??似乎用webpack沒有意義啊!不僅不能忙我壓縮檔案,還把原始檔變胖了。
不要急,我們再看一個例子:
"./src/index.js"
原始檔
require("./page1.js")
let str="index"
console.log(str)
"./src/page1.js"
原始檔
let str="page1"
console.log(str)
- webpack打包後
(function(modules) { // webpackBootstrap
/*此處省略N+1行*/
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({
"./src/index.js": (function(module, exports, __webpack_require__) {
eval("__webpack_require__(/*! ./page1.js */ \"./src/page1.js\")\r\nlet str=\"index\"\r\nconsole.log(str)\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/page1.js":(function(module, exports) {
eval("let str=\"page1\"\r\nconsole.log(str)\n\n//# sourceURL=webpack:///./src/page1.js?");
})
});
當有模組匯入的時候,這個胖JS就展現了他真正的實力。通過__webpack_require__
來實現JS之間匯入的功能。相當於我們不再需要用requirejs,seajs此類包管理器管理我們的前端模組了。webpack幫助我們完成了此類工作。是不是突然覺得這個胖JS不胖了。
webpack的打包原理,就是將各個模組變成字串,存入健值或者陣列之中,然後每個模組之間的關係,通過__webpack_require__
這個方法來實現。最後通過eval
這個函式將字串變成可執行程式碼。
如果大家對__webpack_require__
的實現原理感興趣,可以自己打包一個檔案,不要壓縮混淆,然後研究研究。
對webpack的期許
webpack這個工具,不可能只有打包壓縮這個功能吧。既然是前端工具,那麼必然要具備以下功能:
- 程式碼處理,如打包,編譯等
- 自動生成HTML檔案,比如模板生成頁面
- 本地伺服器,這個是必備功能,不然無法除錯頁面
- 自動編譯程式碼,重新整理瀏覽器,這個大家喜歡稱之為hot replacement(熱替換,熱更新),也就是(修改過的)部分更新
- 那我們逐步來了解下webpack這些功能該如何實現。
webpack從0開始
如果你之前並未使用過webpack,那麼就需要安裝一下webpack,順便學習下如何啟動webpack。
STEP 1 INSTALL
webpack從4開始,webpack分成了兩個包一個webpack一個webpack-cli,所以安裝的時候要安裝兩個包,以及這個包我們是工具,非網站所依賴的包,所以記得放在開發依賴包之中。
npm install webpack webpack-cli -save-dev
也許我們想可以直接安裝webpack,不要webpack-cli。但是現實很殘酷,如果沒有安裝CLI,系統就會告訴你,cli是必不可少的,不然webpack就罷工了。
One CLI for webpack must be installed.
STEP2 RUN
安裝好了之後,我們應該怎麼執行呢?這裡有兩個途徑:
- npm v8.5以上有一個操作叫做
npx
,這個是幹嘛的呢,是幫忙我們直接執行.bin,目錄下的檔案。node_modules\.bin\webpack.cmd
在這個路徑下有webpack的執行命令,我們可以開啟看看。當我們npx webpack
的時候,就是運行了這個檔案。 - 通過配置
package.json
來執行檔案,有個欄位叫做scripts
,我們加一個start
,然後後面跟上命令。到時候我們呼喚npm start
就要可以執行webpack了。
"scripts": {
"start": "webpack --config webpack.config.js"
}
webpack4開始支援零配置,也就是說我不用寫webpack.config.js
也可以執行。那我們就執行試試,結果出現了一個警告:
WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/
這個警告就是告訴我們,webpack4中的mode
引數預設是production
,所以如果是development
的情況就一定要配置了。感覺是零配置似乎是非常牛逼的一個操作,但是實際上還是需要手動配置的,因為這個零配置只是幫我們做掉了一些簡單的事,比如線上就壓縮JS,開發版就不壓縮JS,還有一些預設的路徑之類的。實際上開發的時候,預設的路徑肯定是不夠用的。我們還是老老實實寫配置吧。
我們配置一下,並且執行一下,在開發環境下打包,生成了一個/dist/main.js
檔案。奇怪我的html檔案怎麼沒有打包過來?對,HTML檔案需要我們自己在dist之中建立的,也就是/dist/index.html
。並且路徑要寫好即將生成的JS連結。比如/dist/main.js
在html中引入,我就需要寫成<script src="./main.js"></script>
module.exports = {
mode:"development",
};
這個配置檔案,大家都沒有覺得寫法很熟悉?對!就是CommonJs規範!下一節會詳細解釋webpack.config.js
該如何配置。
webpack的心臟——webpack.config.js
webpack的一切操作都配置在webpack.config.js
之中,可以說配好webpack.config.js
,我們就可以坐等新鮮出爐的網站了。
- Entry:切入點,也就是JS程式入口,按照這個入口開始建立模組依賴,預設
./src/index.js
。 - Output:輸出口,打包程式的安放位置,預設
./dist/main.js
。 - Loaders:載入器,將除了JS和JSON以外的檔案解析載入,比如txt,css等等。
- Plugins:外掛,可以做一些更加牛逼的效果,一般要new一個外掛物件。
- Mode(新增):
production
和development
,這個是webpack4新增的一個屬性,用於區分開發版與線上版,也是很貼心的設定了。
Entry&Output,以及chunk的概念
在學些webpack的配置之前,我們最先接觸的就是輸入Entry和輸出Output的配置。這裡需要引入一個chunk的概念,我們在配置Entry的時候,有時候會設定好多個入口,這每一個入口都是一個chunk,也就是說chunk是根據Entry的配置而來的。大家一定要區分清楚chunk和module的概念啊。module就是程式設計的時候,我們所寫的一塊一塊的功能塊,然後模組之間會有依賴。然後chunk只是將當前模組和他的依賴模組,一起打包起來的程式碼塊。
配置Entry,切入點JS入口也不是件容易的事。
Entry配置
- 單一入口,單個檔案。整個程式只有一個JS,這個配置就很簡單了,我麼也可以不配置,因為預設
./src/index.js
。單個檔案之間傳入字串即可。
entry: '需要打包的JS的相對或者絕對地址'
- 單一入口,多個檔案。有時候我們有好多獨立的JS檔案,但是我只想匯出一個JS,這個時候就需要傳入陣列了。
entry: ["待打包JS-1","待打包JS-2"]
- 多個入口,單個檔案。這個時候我們就要配置健值了,都是預設值,怎麼識別誰是誰。一般來說一個HTML只需要一個chunk,也就是一個入口點。所以這個一般用於多張頁面的配置。
entry: {
JS1: "待打包JS-1",
JS2: "待打包JS-2"
}
- 多個入口,多個檔案。前面提到一個HTML只需要一個入口點,所以這裡我們可以借鑑陣列來完成此操作。
entry: {
JS1: ["待打包JS1-1","待打包JS1-2"],
JS2: ["待打包JS2-1","待打包JS2-2"]
}
Output配置
輸出口,安放打包好的JS,不配置就打包到預設檔案,預設./dist/main.js
。
如果不需要分入口點,整個網站用一個JS。那麼配置一個檔名就可以了。
output: {
filename: 'bundle.js',
}
需要指定資料夾的操作,就再加一個path欄位即可。
output: {
filename: 'bundle.js',
path: __dirname + '/dist'
}
然而現實中,我們不可能只有一個JS,所以這個時候我們就需要配置多個輸出口,不過這個不像entry可以配置健值。但是有一個很簡便的辦法filename: '[name].js'
,檔名我們用[name]
,這樣打包出來的Js檔案就會按照Entry配置的chunk名,來命名了。
當然我們經常回碰到CDN的問題,一個JS會被緩住,這時候我們可以用[hash]
這個引數,來幫我們filename: '[name].[hash].js'
這樣每次生成的JS名就不一樣了。
LOADER,模組的概念
在webpack中,任何檔案都可以變成一個模組,然後被打包到JS之中。但是JS只認識JS,像CSS,或者typescript這類的非標準JS,該如何處理?這個時候Loader就出現了,他幫助webpack將CSS此類檔案變成JS可識別的內容然後打包。所有的loader都需要額外下載安裝,這裡以最常用的CSS為例子,看我們如何將CSS打包到JS之中。
- 安裝css-loader這個載入器
npm install --save-dev css-loader
關於css-loader的用法,大家可以參考下官網。
- 在webpack中配置。大家不要把loader的配置名寫成了loader,他的在webpack中的配置名是
module.rule
。
module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader'},
{ loader: 'css-loader',options: {modules: true}}
]
}
]
}
- 新增style-loader
也就是說loader所有的配置都在rules之下。這裡我還配置了style-loader,那麼我們既然又了css-loader為什麼還要style-loader呢?感覺很累贅啊。那麼接下來就要說說這兩個loader的不同了。
開啟styleloader的官網,我們可以發現:
Adds CSS to the DOM by injecting a
<style>
tag
也就是說style-loader就幹一件事就是將我們處理好的CSS插入到DOM之中,否則我們的CSS只編譯不生效。
如果我們不喜歡內聯樣式,並且覺得CSS檔案沒必要編譯到JS檔案之中,那麼我們可以直接引入一個檔案。我們可以這樣配置。
module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader/url'},
{ loader: "file-loader" }
]
}
]
}
利用style-loader/url
和file-loader
來載入檔案。這個時候會在我們的生產資料夾下新建一個css檔案,然後js中會載入這個新建的css檔案的路徑。我們無需在頁面上配置link,js會幫助我們自動生成一個link,引入我們的css檔案。這樣我們就不用將css和js打包到一起啦。
PLUGINS,更多優化操作
如果說loader只是對於JS的一個操作,比如將CSS轉化到JS之中啦,那麼plugins的功能就更加廣泛,並不侷限載入編譯JS,比如HTML檔案的操作。
這裡有一個我剛開始的遇到的問題,就是:
webpack主要是負責JS的編譯管理,那麼我的HTML檔案呢?難道要我一個個在dist之中建立好嗎??
這個時候HTML Webpack Plugin出現啦,這個外掛是專門用於建立管理HTML的。
首先是安裝npm i --save-dev html-webpack-plugin
,然後是配置webpack:
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode:"development",
plugins: [
new HtmlWebpackPlugin()
]
}
一般外掛都是建立一個新的例項,然後加入plugins這個陣列之中。
然後我們來看看這個HtmlWebpackPlugin外掛,這個外掛很強大,我們不僅可以控制模版,還可以配置頁面內容,像下方這樣。
test.html
<body>
<%= htmlWebpackPlugin.options.title %>
</body>
webpack.config.js
plugins: [
new HtmlWebpackPlugin(), //生成自動的index.html
new HtmlWebpackPlugin({ // 生成一個test.html
title: 'Custom template using Handlebars',
filename: 'test.html',
template: path.join(__dirname,'src/test.html')
})
],
由上述例子可以看出,為了保證外掛的靈活性,比如我每個頁面的配置不一樣,我們就可以new好幾個外掛來處理我們的html檔案。一個例項處理一個頁面。
webpack4.0的新特色——mode
MODE有三個引數production
,development
,none
,前兩個是有預設的外掛,而最後一個則是什麼都沒有,也就是說設定為none
的話,webpack就是最初的樣子,無任何預設,需要從無到有開始配置。
我們來研究下他們之間的配置的區別,首先是兩者都有的一個new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development|production") })
,這個是用來讓我們可以直接在js中引用"process.env.NODE_ENV"
的值,這樣就可以在JS之中通過這個值來區別開發板與先上版本的不同指令碼。
編譯之前的index.js
console.log(process.env.NODE_ENV)
編譯之後的index.js
console.log("development")
我們可以看到直接將我們的process.env.NODE_ENV
替換成了所以定義的內容。這個小功能可以幫助我們在寫業務JS的時候,區分線上版本與開發版本。
development
我們接著看看其他的開發中使用的外掛NamedModulesPlugin
和NamedChunksPlugin
,原本我們的webpack並不會給打包的模組加上姓名,一般都是按照序號來,從0開始,然後載入第幾個模組。這個對機器來說無所謂,查詢載入很快,但是對於人腦來說就是災難了,所以這個時候給各個module和chunk加上姓名,便於開發的時候查詢。
在沒有mode的情況下,這些外掛需要自己配置,而有了mode之後,我們的配置就可以省略了。
// webpack.development.config.js
module.exports = {
+ mode: 'development'
- devtool: 'eval',
- plugins: [
- new webpack.NamedModulesPlugin(),
- new webpack.NamedChunksPlugin(),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
- ]
}
production
在線上版本中,我們第一個需要處理的就要混淆&壓縮JS了吧。在線上mode中,自帶JS混淆壓縮,可以說這個功能很方便了。
// webpack.production.config.js
module.exports = {
+ mode: 'production',
- plugins: [
- new UglifyJsPlugin(/* ... */),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
- new webpack.optimize.ModuleConcatenationPlugin(),
- new webpack.NoEmitOnErrorsPlugin()
- ]
}
本地伺服器&&hot refresh
官方文件一共給出了3中實時編譯的方法:--watch
,webpack-dev-server``和
webpack-dev-middleware`
--watch |
webpack-dev-server |
webpack-dev-middleware |
|
---|---|---|---|
實時編譯 | yes | yes | yes |
伺服器 | no | yes | yes |
hot | no | yes | yes |
程式碼上手 | 簡單 | 中等 | 稍困難 |
--watch
--watch是個好方法,執行之後,會自動給我們編譯檔案。但是瀏覽器需要手動重新整理才能出現最新的內容。
webpack-dev-server
webpack-dev-server
雖然,可以直接在config中配置引數,但是還是需要安裝一下,才可以使用。
npm install --save-dev webpack-dev-server
webpack不產出任何編譯後的檔案。他只提供記憶體中的程式碼,假裝是真是的程式碼。如果你希望在其他目錄中讀取檔案。可以更換publicPath 選項。每次修改都會實時編譯。
但是使用webpack-dev-server
,修改檔案,並不會實時重新整理瀏覽器,我們需要一些配置才可以。
首先需要在pligns中加入new webpack.HotModuleReplacementPlugin()
。
plugins: [
new webpack.HotModuleReplacementPlugin()
],
devServer: {
contentBase: path.join(__dirname, 'dist'),
publicPath:"/",
},
webpack-dev-server
雖然很方便,配置也簡單,但是他編譯出來的檔案與npx webpack
編譯出來的並不一樣,因此除錯起來未必很方便。
webpack-dev-middleware
看見middleware就應該知道這個是一箇中間件,用於連結webpack的編譯功能和其他nodejs伺服器框架的橋樑,這邊我們選擇express這個框架。
首先是安裝這兩個包。
npm install --save-dev express webpack-dev-middleware
這個比webpack-dev-server
要複雜一些,還需要安裝一個express。但是這個的編譯的內容是會寫入dist檔案的,實時更新,完全按照webpack
的編譯來。她的原理就是先執行webpack,在更新到伺服器上,這樣我們訪問的就是最新的內容了。
既然是中介軟體,那麼就不是webpack親生的,就需要在webpack-dev-server
配置的基礎上加點料。
我們要在需要監控的入口點加入監控的js,像這樣寫:
entry:{
index:['webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=true',path.join(__dirname,"/src/index.js")],
},
接著就是server.js的編寫,想要寫好這一部分,大家要先學會express,以及express中介軟體的用法。然後再是將webpack掛載到express之上。
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
}));
app.use(require("webpack-hot-middleware")(compiler));
app.listen(8080, function () {
console.log('Example app listening on port 8080!\n');
});
這樣配置雖然麻煩,但是我們能看到實時編譯的JS檔案,對於網站的整體細節把控會更好。
總結
感覺寫了一篇超長的入門文章,列出了webpack的配置用法,以及webpack外掛的用法,可以說webpack外掛是webpack之魂,擴充套件了許多其他的功能。還有如何實時編譯我們的網站。