webpack:從入門到真實項目配置
本文原載於掘金,作者夕陽(飢人谷學員),轉載已獲作者授權。
該文使用的 Webpack 版本為 3.6.0,本文分兩部分。第一步是簡單的使用 webpack,第二部分通過一個真實項目來配置 webpack,沒有使用任何的 CLI,都是一步步配置直到完成生產代碼的打包。這是本項目對應的倉庫,每個小節基本都對應了一次 commit。
這是本文的大綱,如果覺得有興趣你就可以往下看了
Webpack 到底是什麼
自從出現模塊化以後,大家可以將原本一坨代碼分離到個個模塊中,但是由此引發了一個問題。每個 JS 文件都需要從伺服器去拿,由此會導致載入速度變慢。Webpack 最主要的目的就是為了解決這個問題,將所有小文件打包成一個或多個大文件,官網的圖片很好的詮釋了這個事情,除此之外,Webpack 也是一個能讓你使用各種前端新技術的工具。
簡單使用
安裝
在命令行中依次輸入
mkdir webpack-demoncd webpack-demon// 創建 package.json,這裡會問一些問題,直接回車跳過就行nnpm init n// 推薦這個安裝方式,當然你也安裝在全局環境下n// 這種安裝方式會將 webpack 放入 devDependencies 依賴中nnpm install --save-dev webpackn
然後按照下圖創建文件
在以下文件寫入代碼
// sum.jsn// 這個模塊化寫法是 node 環境獨有的,瀏覽器原生不支持使用nmodule.exports = function(a, b) {n return a + bn}n// index.jsnvar sum = require(./sum)nconsole.log(sum(1, 2))n<!DOCTYPE html>n<html lang="en">n<head>n <title>Document</title>n</head>n<body>n <div id="app"></div>n <script src="./build/bundle.js"></script>n</body>n</html>n
現在我們開始配置最簡單的 webpack,首先創建 webpack.config.js 文件,然後寫入如下代碼
// 自帶的庫nconst path = require(path)nmodule.exports = {n entry: ./app/index.js, // 入口文件n output: {n path: path.resolve(__dirname, build), // 必須使用絕對地址,輸出文件夾n filename: "bundle.js" // 打包後輸出文件的文件名n }n }n
現在我們可以開始使用 webpack 了,在命令行中輸入
node_modules/.bin/webpackn
沒問題的話你應該可以看到類似的樣子
可以發現原本兩個 JS 文件只有 100B,但是打包後卻增長到 2.66KB,這之中 webpack 肯定做了什麼事情,我們去 bundle.js 文件中看看。
把代碼簡化以後,核心思路是這樣的
var array = [(function () {n var sum = array[1]n console.log(sum(1, 2))n }),n (function (a,b) {n return a + bn })n]narray[0]() // -> 3n
因為 module.export 瀏覽器是不支持的,所以 webpack 將代碼改成瀏覽器能識別的樣子。現在將 index.html 文件在瀏覽器中打開,應該也可以看到正確的 log。
我們之前是在文件夾中安裝的 webpack,每次要輸入 node_modules/.bin/webpack 過於繁瑣,可以在 package.json 如下修改
"scripts": {n "start": "webpack"n },n
然後再次執行 npm run start,可以發現和之前的效果是相同的。簡單的使用到此為止,接下來我們來探索 webpack 更多的功能。
Loader
Loader 是 webpack 一個很強大功能,這個功能可以讓你使用很多新的技術。
Babel
Babel 可以讓你使用 ES2015/16/17 寫代碼而不用顧忌瀏覽器的問題,Babel 可以幫你轉換代碼。首先安裝必要的幾個 Babel 庫
npm i --save-dev babel-loader babel-core babel-preset-envn
先介紹下我們安裝的三個庫
- babel-loader 用於讓 webpack 知道如何運行 babel
- babel-core 可以看做編譯器,這個庫知道如何解析代碼
- babel-preset-env 這個庫可以根據環境的不同轉換代碼
接下來更改 webpack-config.js 中的代碼
module.exports = {n// ......n module: {n rules: [n {n // js 文件才使用 babeln test: /.js$/,n // 使用哪個 loadern use: babel-loader,n // 不包括路徑n exclude: /node_modules/n }n ]n }n}n
配置 Babel 有很多方式,這裡推薦使用 .babelrc 文件管理。
// ..babelrcn{n "presets": ["babel-preset-env"]n}n
現在將之前 JS 的代碼改成 ES6 的寫法
// sum.jsnexport default (a, b) => {n return a + bn}n// index.jsnimport sum from ./sumnconsole.log(sum(1, 2))n
執行 npm run start,再觀察 bundle.js 中的代碼,可以發現代碼被轉換過了,並且同樣可以正常 輸出3。
當然 Babel 遠不止這些功能,有興趣的可以前往官網自己探索。
處理圖片
這一小節我們將使用 url-loader 和 file-loader,這兩個庫不僅可以處理圖片,還有其他的功能,有興趣的可以自行學習。
先安裝庫
npm i --save-dev url-loader file-loadern
創建一個 images 文件夾,放入兩張圖片,並且在 app 文件夾下創建一個 js 文件處理圖片
,目前的文件夾結構如圖// addImage.jsnlet smallImg = document.createElement(img)n// 必須 require 進來nsmallImg.src = require(../images/small.jpeg)ndocument.body.appendChild(smallImg)nnlet bigImg = document.createElement(img)nbigImg.src = require(../images/big.jpeg)ndocument.body.appendChild(bigImg)n
接下來修改 webpack.config.js 代碼
module.exports = {n// ...n module: {n rules: [n // ...n {n // 圖片格式正則n test: /.(png|jpe?g|gif|svg)(?.*)?$/,n use: [n {n loader: url-loader,n // 配置 url-loader 的可選項n options: {n // 限制 圖片大小 10000B,小於限制會將圖片轉換為 base64格式n limit: 10000,n // 超出限制,創建的文件格式n // build/images/[圖片名].[hash].[圖片格式]n name: images/[name].[hash].[ext]n }n }n ]n }n ]n }n }n
運行 npm run start,打包成功如下圖
可以發現大的圖片被單獨提取了出來,小的圖片打包進了 bundle.js 中。
在瀏覽器中打開 HTML 文件,發現小圖確實顯示出來了,但是卻沒有看到大圖,打開開發者工具欄,可以發現我們大圖的圖片路徑是有問題的,所以我們又要修改 webpack.config.js 代碼了。
module.exports = {n entry: ./app/index.js, // 入口文件n output: {n path: path.resolve(__dirname, build), // 必須使用絕對地址,輸出文件夾n filename: "bundle.js", // 打包後輸出文件的文件名n publicPath: build/ // 知道如何尋找資源n }n // ...n }n
最後運行下 npm run start,編譯成功了,再次刷新下頁面,可以發現這次大圖被正確的顯示了。下一小節我們將介紹如何處理 CSS 文件。
處理 CSS 文件
添加 styles 文件夾,新增 addImage.css 文件,然後在該文件中新增代碼
img {n border: 5px black solid;n}n.test {border: 5px black solid;}n
這一小節我們先使用 css-loader 和 style-loader 庫。前者可以讓 CSS 文件也支持 impost,並且會解析 CSS 文件,後者可以將解析出來的 CSS 通過標籤的形式插入到 HTML 中,所以後面依賴前者。
npm i --save-dev css-loader style-loadern
首先修改 addImage.js 文件
import ../styles/addImage.cssnnlet smallImg = document.createElement(img)nsmallImg.src = require(../images/small.jpeg)ndocument.body.appendChild(smallImg)nn// let bigImg = document.createElement(img)n// bigImg.src = require(../images/big.jpeg)n// document.body.appendChild(bigImg)n
然後修改 webpack.config.js 代碼
module.exports = {n// ...n module: {n rules: [n {n test: /.css$/,n use: [style-loader,n {n loader: css-loader,n options: {n modules: truen }n }n ]n },n ]n }n }n
運行下 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")nnmodule.exports = {n// ....n module: {n rules: [n {n test: /.css$/,n // 寫法和之前基本一致n loader: ExtractTextPlugin.extract({n // 必須這樣寫,否則會報錯n fallback: style-loader,n use: [{n loader: css-loader,n options: { n modules: truen }n }]n })n ]n }n ]n },n // 插件列表n plugins: [n // 輸出的文件路徑n new ExtractTextPlugin("css/[name].[hash].css")n ]n }n
運行下 npm run start,可以發現 CSS 文件被單獨打包出來了
但是這時候刷新頁面會發現圖片的邊框消失了,那是因為我們的 HTML 文件沒有引用新的 CSS 文件,所以這裡需要我們手動引入下,在下面的章節我們會通過插件的方式自動引入新的文件。
接下來,會用一個項目來繼續我們的 webpack 學習,在這之前,先 clone 一下項目。該項目原地址是 這裡,因為使用的 webpack 版本太低,並且依賴的庫也有點問題,故我將項目拷貝了過來並修改了幾個庫的版本號。
請依次按照以下代碼操作
git clone https://github.com/KieSun/webpack-demo.gitncd webpack-demon// 切換到 0.1 標籤上並創建一個新分支ngit checkout -b demo 0.1n// 查看分支是否為 demo,沒問題的話就可以進行下一步ngstn
如何在項目中使用 webpack
項目中已經配置了很簡單的 babel 和 webpack,直接運行 npm run start 即可
這時候你會發現這個 bundle.js 居然有這麼大,這肯定是不能接受的,所以接下來章節的主要目的就是將單個文件拆分為多個文件,優化項目。
分離代碼
先讓我們考慮下緩存機制。對於代碼中依賴的庫很少會去主動升級版本,但是我們自己的代碼卻每時每刻都在變更,所以我們可以考慮將依賴的庫和自己的代碼分割開來,這樣用戶在下一次使用應用時就可以盡量避免重複下載沒有變更的代碼,那麼既然要將依賴代碼提取出來,我們需要變更下入口和出口的部分代碼。
// 這是 packet.json 中 dependencies 下的nconst VENOR = ["faker",n "lodash",n "react",n "react-dom",n "react-input-range",n "react-redux",n "redux",n "redux-form",n "redux-thunk"n]nnmodule.exports = {n// 之前我們都是使用了單文件入口n// entry 同時也支持多文件入口,現在我們有兩個入口n// 一個是我們自己的代碼,一個是依賴庫的代碼n entry: {n // bundle 和 vendor 都是自己隨便取名的,會映射到 [name] 中n bundle: ./src/index.js,n vendor: VENORn },n output: {n path: path.join(__dirname, dist),n filename: [name].jsn },n // ...n }n
現在我們 build 一下,看看是否有驚喜出現
真的有驚喜。。為什麼 bundle 文件大小壓根沒變。這是因為 bundle 中也引入了依賴庫的代碼,剛才的步驟並沒有抽取 bundle 中引入的代碼,接下來讓我們學習如何將共同的代碼抽取出來。
抽取共同代碼
在這小節我們使用 webpack 自帶的插件 CommonsChunkPlugin。
module.exports = {n//...n output: {n path: path.join(__dirname, dist),n // 既然我們希望緩存生效,就應該每次在更改代碼以後修改文件名n // [chunkhash]會自動根據文件是否更改而更換哈希n filename: [name].[chunkhash].jsn },n plugins: [n new webpack.optimize.CommonsChunkPlugin({n // vendor 的意義和之前相同n // manifest文件是將每次打包都會更改的東西單獨提取出來,保證沒有更改的代碼無需重新打包,這樣可以加快打包速度n names: [vendor, manifest],n // 配合 manifest 文件使用n minChunks: Infinityn })n ]n};n
當我們重新 build 以後,會發現 bundle 文件很明顯的減小了體積
但是我們使用哈希來保證緩存的同時會發現每次 build 都會生成不一樣的文件,這時候我們引入另一個插件來幫助我們刪除不需要的文件。
npm install --save-dev clean-webpack-pluginn
然後修改配置文件
module.exports = {n//...n plugins: [n // 只刪除 dist 文件夾下的 bundle 和 manifest 文件n new CleanWebpackPlugin([dist/bundle.*.js,dist/manifest.*.js], {n // 列印 logn verbose: true,n // 刪除文件n dry: falsen }),n ]n};n
然後 build 的時候會發現以上文件被刪除了。
因為我們現在將文件已經打包成三個 JS 了,以後也許會更多,每次新增 JS 文件我們都需要手動在 HTML 中新增標籤,現在我們可以通過一個插件來自動完成這個功能。
npm install html-webpack-plugin --save-devn
然後修改配置文件
module.exports = {n//...n plugins: [n // 我們這裡將之前的 HTML 文件當做模板n // 注意在之前 HTML 文件中請務必刪除之前引入的 JS 文件n new HtmlWebpackPlugin({n template: index.htmln })n ]n};n
執行 build 操作會發現同時生成了 HTML 文件,並且已經自動引入了 JS 文件
按需載入代碼
在這一小節我們將學習如何按需載入代碼,在這之前的 vendor 入口我發現忘記加入 router 這個庫了,大家可以加入這個庫並且重新 build 下,會發現 bundle 只有不到 300KB 了。
現在我們的 bundle 文件包含了我們全部的自己代碼。但是當用戶訪問我們的首頁時,其實我們根本無需讓用戶載入除了首頁以外的代碼,這個優化我們可以通過路由的非同步載入來完成。
現在修改 src/router.js
// 注意在最新版的 V4路由版本中,更改了按需載入的方式,如果安裝了 V4版,可以自行前往官網學習nimport React from react;nimport { Router, Route, IndexRoute, hashHistory } from react-router;nnimport Home from ./components/Home;nimport ArtistMain from ./components/artists/ArtistMain;nnconst rootRoute = {n component: Home,n path: /,n indexRoute: { component: ArtistMain },n childRoutes: [n {n path: artists/new,n getComponent(location, cb) {n System.import(./components/artists/ArtistCreate)n .then(module => cb(null, module.default))n }n },n {n path: artists/:id/edit,n getComponent(location, cb) {n System.import(./components/artists/ArtistEdit)n .then(module => cb(null, module.default))n }n },n {n path: artists/:id,n getComponent(location, cb) {n System.import(./components/artists/ArtistDetail)n .then(module => cb(null, module.default))n }n }n ]n}nnconst Routes = () => {n return (n <Router history={hashHistory} routes={rootRoute} />n );n};nnexport default Routes;n
然後執行 build 命令,可以發現我們的 bundle 文件又瘦身了,並且新增了幾個文件
將 HTML 文件在瀏覽器中打開,當點擊路由跳轉時,可以在開發者工具中的 Network 一欄中看到載入了一個 JS 文件。
首頁
點擊右上角 Random Artist 以後
自動刷新
每次更新代碼都需要執行依次 build,並且還要等上一會很麻煩,這一小節介紹如何使用自動刷新的功能。
首先安裝插件
npm i --save-dev webpack-dev-servern
然後修改 packet.json 文件
"scripts": {n "build": "webpack",n "dev": "webpack-dev-server --open"n },n
現在直接執行 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-pluginn
修改 webpack 配置
var webpack = require(webpack);nvar path = require(path);nvar HtmlWebpackPlugin = require(html-webpack-plugin)nvar CleanWebpackPlugin = require(clean-webpack-plugin)nvar ExtractTextPlugin = require(extract-text-webpack-plugin)nvar OptimizeCSSPlugin = require(optimize-css-assets-webpack-plugin)nnconst VENOR = ["faker",n "lodash",n "react",n "react-dom",n "react-input-range",n "react-redux",n "redux",n "redux-form",n "redux-thunk",n "react-router"n]nnmodule.exports = {n entry: {n bundle: ./src/index.js,n vendor: VENORn },n // 如果想修改 webpack-dev-server 配置,在這個對象裡面修改n devServer: {n port: 8081n },n output: {n path: path.join(__dirname, dist),n filename: [name].[chunkhash].jsn },n module: {n rules: [{n test: /.js$/,n use: babel-loadern },n {n test: /.(png|jpe?g|gif|svg)(?.*)?$/,n use: [{n loader: url-loader,n options: {n limit: 10000,n name: images/[name].[hash:7].[ext]n }n }]n },n {n test: /.css$/,n loader: ExtractTextPlugin.extract({n fallback: style-loader,n use: [{n // 這邊其實還可以使用 postcss 先處理下 CSS 代碼n loader: css-loadern }]n })n },n ]n },n plugins: [n new webpack.optimize.CommonsChunkPlugin({n name: [vendor, manifest],n minChunks: Infinityn }),n new CleanWebpackPlugin([dist/*.js], {n verbose: true,n dry: falsen }),n new HtmlWebpackPlugin({n template: index.htmln }),n // 生成全局變數n new webpack.DefinePlugin({n "process.env.NODE_ENV": JSON.stringify("process.env.NODE_ENV")n }),n // 分離 CSS 代碼n new ExtractTextPlugin("css/[name].[contenthash].css"),n // 壓縮提取出的 CSS,並解決ExtractTextPlugin分離出的 JS 重複問題n new OptimizeCSSPlugin({n cssProcessorOptions: {n safe: truen }n }),n // 壓縮 JS 代碼n new webpack.optimize.UglifyJsPlugin({n compress: {n warnings: falsen }n })n ]n};n
修改 packet.json 文件
"scripts": {n "build": "NODE_ENV=production webpack -p",n "dev": "webpack-dev-server --open"n }n
執行 npm run build
可以看到我們在經歷了這麼多步以後,將 bundle 縮小到了只有 27.1KB,像 vendor 這種常用的庫我們一般可以使用 CDN 的方式外鏈進來。
補充
webpack 配置上有些實用的小點在上文沒有提到,統一在這裡提一下。
module.exports = {n resolve: {n // 文件擴展名,寫明以後就不需要每個文件寫後綴n extensions: [.js, .css, .json],n // 路徑別名,比如這裡可以使用 css 指向 static/css 路徑n alias: {n @: resolve(src),n css: resolve(static/css)n }n },n // 生成 source-map,用於打斷點,這裡有好幾個選項n devtool: #cheap-module-eval-source-map,n}n
後記
如果你是跟著本文一個個步驟敲下來的,那麼大部分的 webpack 配置你應該都是可以看懂了,並且自己應該也知道如何去配置。謝謝大家看到這裡,這是本項目對應的倉庫,每個小節基本都對應了一次 commit。
文章較長,有錯誤也難免,如果你發現了任何問題或者我有任何錶述的不明白的地方,都可以留言給我。
—————————end———————————
加飢人谷官方微信號: hungervalley ,暗號:來自知乎
每日一題,每周資源推薦,精彩博客推薦,工作、筆試、面試經驗交流解答,免費直播課,群友輕分享... 數不盡的福利免費送
推薦閱讀:
※簡單幾步助你優化React應用包體
※讀懂webpack生成的26行代碼
※webpack 打包JS 的運行原理
※淺析 Webpack 插件化設計
※Webpack工程化解決方案easywebpack