用於前端開發的webpack4配置[帶註釋]

Leeli發表於2018-12-08

❤️覺得不錯點個贊喲❤️。原文連結

隨著web開發變得越來越複雜,我們需要使用工具來幫助我們構建現代化網站。這是一個完整通過複雜webpack4配置的線上生產例子。

用於前端開發的webpack4配置[帶註釋]

構建現代化網站已經成為自定義應用程式開發,網站期望能做的更多,具有傳統應用的功能,而不僅僅是一個推廣網站。

隨著一個流程變得複雜時,我們就會將它分解為可管理的元件,並使用工具進行自動化構建,比如製造汽車、起草法案[法律檔案]、建立網站。

使用正確的工具完成工作

像webpack這樣的工具一直處於現代web開發的最前沿,正是因為如此,它幫助我們構建複雜的事物。

webpack4擁有一些意想不到的改進,最吸引我的就是它在構建速度上面變得有多快,所以我決定採用它。

用於前端開發的webpack4配置[帶註釋]

hold住,因為這是一篇充滿大量資訊的長篇文章。

採用webpack

一年多以前,我發表了一篇文章: A Gulp Workflow for Frontend Development Automation[用於前端自動化的gulp工作流],講解了如何使用gulp完成同樣的事情。然而在這段時間裡,我越來越多地使用像Vue-JSGraphQL這樣的前端框架,如Using VueJS + GraphQL to make Practical Magic這篇文章。

我發現webpack讓我更容易的去構建各種型別的網站以及應用程式,而且它也允許我使用最現代化的工具鏈。

還有其他選擇:

  • Laravel Mix是基於webpack的構建工具層,它十分簡潔,你可以快速啟動並執行,它可以在90%的時間內完成你想要的任務,但剩下的10%無論如何都會進入到webpack,目前還不支援webpack4。

  • 如果你只是用VueJS前端框架,那麼使用vue-cli是個不錯的選擇,它也是基於webpack,大部分時間都可以工作,並且為你做一些意想不到的事情。但同樣的,當它提供的功能已經滿足不了你的需求,你還是需要用到webpack,而且我並不是只使用VueJS。

  • Neutrino也是基於webpack,我們可以關注部落格:Neutrino: How I Learned to Stop Worrying and Love Webpack。神奇的點就是它可以通過像搭樂高積木一樣去配置webpack,但學習使用讓的成本跟學習webpack其實差不了多少。

如果你選擇上述工具(或者其他工具),我不會對你提出任:它們都是基於webpack封裝。

理解開發系統中層是如何工作的是有好處的。

最終,你只需要決定你希望站在前端技術金字塔中的哪個位置。

某些時候,我認為了解像webpack這樣重要的工具是如何工作是有意義的。不久前,我向Sean Larkin(webpack核心團隊成員之一)抱怨說webpack就像一個“黑匣子”,他的回答簡潔卻非常精闢:

It’s only black if you haven’t opened it.[如果你沒有開啟這個所謂的“黑匣子”,它永遠都是未知的。]

他說的對,是時候開啟“黑匣子”了。

本文不會教你所有關於webpack的知識,甚至是如何安裝它,下面有很多、資料給你選擇,你可以選擇你認為不錯的方式:

這樣的資料還有很多,相反地本文將用webpack4配置一個複雜的完整工作例子,並新增註釋。你可以使用完整的示例,也可以使用它的部分配置項,但希望你可以從中學到一些東西。在我學習webpack的過程中,我發現有很多教程視訊,一堆文章給你將如何安裝它並新增一些基礎配置,但卻大部分沒有實際線上生產環境的webpack配置示例,所以我寫了這篇文章。

WHAT WE GET OUT OF THE BOX

當我開始通過開啟“黑匣子”來學習webpack時,我有一份我依賴的技術列表,我想將它成為構建流程的一部分。我也會花時間四處看看,看看在這個過程中,我還能採用什麼。

正如在文章 A Pretty Website Isn’t Enough article討論的那樣,網站效能一直都是我關注的重點,所以在配置webpack過程中關注效能問題也很正常。

用於前端開發的webpack4配置[帶註釋]

所以這是我想用webpack為我處理的事情,以及我希望在構建過程中加入的技術:

  • Development / Production —— 在本地開發中,我通過webpack-dev-server進行快速構建,對於生產環境的構建(通常通過buddy.works在Docker容器中構建),我希望儘可能優化每一個點。因此,我們區分devprod的配置以及構建。

  • Hot Module Replacement —— 當我修改了js、css或者頁面,我希望網頁能夠自動重新整理,大幅度提高了開發效率:不需要你去點瀏覽器重新整理按鈕。

  • Dynamic Code Splitting —— 我不想手動在配置檔案中定義js chunk,所以我讓webpack幫我解決這個問題。

  • Lazy Loading —— 又稱非同步動態模組載入,在需要時載入所需的程式碼資源。

  • Modern &
    Legacy JS Bundles
    —— 我想將es2015 + JavaScript模組釋出到能夠支援全球75%以上的瀏覽器上,同時為低版本的瀏覽器提供一個補丁包(包括所有轉碼和polyfills)。

  • Cache Busting via manifest.json —— 可以讓我們為靜態資源設定快取,同時保證它們在更改使自動重新快取。

  • Critical CSS —— 根據文章Implementing Critical CSS on your website,可以提高首頁面的載入速度。

  • Workbox Service Worker —— 我們可以使用Google的Workbox專案為我們建立一個Service Worker ,瞭解我們專案的所有東西[這句翻譯的有點問題,可以看原文理解]。PWA,我們來了!

  • PostCSS —— 我認為它是“css的babel”,像sass和scss都是基於它來構建,它讓你可以使用即將推出的css功能。

  • Image Optimization —— 目前,圖片仍然是大部分網頁呈現的主要內容,所以可以通過mozjpegoptipngsvgo等自動化工具來壓縮優化圖片資源是很有必要的。

  • Automatic .webp Creation —— Chrome、Edge和FireFox都支援.webp檔案,它比jpeg體積更小,節省資源。

  • VueJS —— VueJs是我這次用的前端框架,我希望能夠通過單個檔案.vue元件作為開發過程的一部分。

  • Tailwind CSS —— Tailwind是一個實用程式優先的css,我用它在本地開發中快速進行原型設計,然後通過PurgeCss進行生產,從而減小體積。

哇,看起來相當豐富的清單!

用於前端開發的webpack4配置[帶註釋]

還有很多東西,比如JavaScript自動化、css壓縮以及其他標準配置,去構建我們期望的前端系統。

我還希望它可以給開發團隊使用,開發團隊可以使用不同的工具應用在他們的本地開發環境,並使配置易於維護以及可以被其他專案重用。

The importance of maintainability and reusability can’t be understated [可維護性和複用性是非常重要的。]

你使用的前端框架或者技術棧可以跟我的不一樣,但應用的規則其實是相同的,所以請繼續閱讀其餘部分,不管你用的是什麼技術棧!

PROJECT TREE &
ORGANIZATION

為了讓你瞭解程式的整體架構,這裡展示一個簡單的專案樹:

├── example.env├── package.json├── postcss.config.js├── src│   ├── css│   │   ├── app.pcss│   │   ├── components│   │   │   ├── global.pcss│   │   │   ├── typography.pcss│   │   │   └── webfonts.pcss│   │   ├── pages│   │   │   └── homepage.pcss│   │   └── vendor.pcss│   ├── fonts│   ├── img│   │   └── favicon-src.png│   ├── js│   │   ├── app.js│   │   └── workbox-catch-handler.js│   └── vue│       └── Confetti.vue├── tailwind.config.js├── templates├── webpack.common.js├── webpack.dev.js├── webpack.prod.js├── webpack.settings.js└── yarn.lock複製程式碼

完整的程式碼可以檢視: annotated-webpack-4-config

在核心配置檔案方法,包括:

  • .env—— webpack-dev-server特定於開發環境的設定,不需要在git中檢查

  • webpack.settings.js —— 一個JSON-ish設定檔案,我們需要在專案之間編輯的唯一檔案

  • webpack.common.js —— 相同型別的構建放在統一設定檔案

  • webpack.dev.js —— 設定本地開發各個構建

  • webpack.prod.js —— 設定生產環境各個構建

這是一個如何將以上配置組合成的圖表:

用於前端開發的webpack4配置[帶註釋]

目標是你只需要編輯專案之間的金色圓角區域(.env&
webpack.settings.js)。

以這種形式分離出來使得配置檔案使用變得更加容易,即使你最終修改了我原先提供的各種webpack配置檔案,但保持這種方式有助於你長期去對配置檔案進行維護。

彆著急,我們等下會詳細介紹每個檔案。

ANNOTATED PACKAGE.JSON

讓我們從修改我們的package.json開始入手:

{ 
"name": "example-project", "version": "1.0.0", "description": "Example Project brand website", "keywords": [ "Example", "Keywords" ], "homepage": "https://github.com/example-developer/example-project", "bugs": {
"email": "someone@example-developer.com", "url": "https://github.com/example-developer/example-project/issues"
}, "license": "SEE LICENSE IN LICENSE.md", "author": {
"name": "Example Developer", "email": "someone@example-developer.com", "url": "https://example-developer.com"
}, "browser": "/web/index.php", "repository": {
"type": "git", "url": "git+https://github.com/example-developer/example-project.git"
}, "private": true,複製程式碼

這裡沒什麼有趣的東西,只是包含了我們網站的元資訊,就像package.json規範中所述。

"scripts": { 
"dev": "webpack-dev-server --config webpack.dev.js", "build": "webpack --config webpack.prod.js --progress --hide-modules"
},複製程式碼

上述指令碼代表了我們為專案提供的兩個主要構建步驟:

  • dev —— 只要我們修改了專案的程式碼,啟動該配置後,它會使用webpack-dev-server來實現熱模組替換(HMR),記憶體編譯以及其他細節處理。

  • build —— 當我們進行生產部署時,它會執行所有花哨以及耗時的事情,例如Critical CSS、JavaScript壓縮等。

我們只需要在命令列執行以下操作:如果我們使用的是yarn,輸入yarn dev或者yarn build;如果使用的是npm,輸入npm run dev或者npm run build。這些是你唯一需要使用的兩個命令。

請注意,不僅可以通過--config配置,我們還可以傳入單獨的配置檔案進行配置。這樣我們可以將webpack配置分解為單獨的邏輯檔案,因為與生產環境構建相比,我們將為開發環境的構建做很多不同的事情。

接下來我們的browserslist配置:

"browserslist": { 
"production": [ ">
1%"
, "last 2 versions", "Firefox ESR" ], "legacyBrowsers": [ ">
1%"
, "last 2 versions", "Firefox ESR" ], "modernBrowsers": [ "last 2 Chrome versions", "not Chrome <
60"
, "last 2 Safari versions", "not Safari <
10.1"
, "last 2 iOS versions", "not iOS <
10.3"
, "last 2 Firefox versions", "not Firefox <
54"
, "last 2 Edge versions", "not Edge <
15"
]
},複製程式碼

這是一個基於人類可讀配置的特定瀏覽器列表PostCSS autoprefixer預設使用在production設定中,我們將legacyBrowsersmodernBrowsers傳遞給Babel用來處理傳統[過去]和現代js包的構建[處理轉碼問題,相容es6等寫法],後面會有詳細介紹。

接著是devDependencies,它是構建系統所需的所有npm包:

"devDependencies": { 
"@babel/core": "^7.1.0", "@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-transform-runtime": "^7.1.0", "@babel/preset-env": "^7.1.0", "@babel/register": "^7.0.0", "@babel/runtime": "^7.0.0", "autoprefixer": "^9.1.5", "babel-loader": "^8.0.2", "clean-webpack-plugin": "^0.1.19", "copy-webpack-plugin": "^4.5.2", "create-symlink-webpack-plugin": "^1.0.0", "critical": "^1.3.4", "critical-css-webpack-plugin": "^0.2.0", "css-loader": "^1.0.0", "cssnano": "^4.1.0", "dotenv": "^6.1.0", "file-loader": "^2.0.0", "git-rev-sync": "^1.12.0", "glob-all": "^3.1.0", "html-webpack-plugin": "^3.2.0", "ignore-loader": "^0.1.2", "imagemin": "^6.0.0", "imagemin-gifsicle": "^5.2.0", "imagemin-mozjpeg": "^7.0.0", "imagemin-optipng": "^5.2.1", "imagemin-svgo": "^7.0.0", "imagemin-webp": "^4.1.0", "imagemin-webp-webpack-plugin": "^1.0.2", "img-loader": "^3.0.1", "mini-css-extract-plugin": "^0.4.3", "moment": "^2.22.2", "optimize-css-assets-webpack-plugin": "^5.0.1", "postcss": "^7.0.2", "postcss-extend": "^1.0.5", "postcss-hexrgba": "^1.0.1", "postcss-import": "^12.0.0", "postcss-loader": "^3.0.0", "postcss-nested": "^4.1.0", "postcss-nested-ancestors": "^2.0.0", "postcss-simple-vars": "^5.0.1", "purgecss-webpack-plugin": "^1.3.0", "purgecss-whitelister": "^2.2.0", "resolve-url-loader": "^3.0.0", "sane": "^4.0.1", "save-remote-file-webpack-plugin": "^1.0.0", "style-loader": "^0.23.0", "symlink-webpack-plugin": "^0.0.4", "terser-webpack-plugin": "^1.1.0", "vue-loader": "^15.4.2", "vue-style-loader": "^4.1.2", "vue-template-compiler": "^2.5.17", "webapp-webpack-plugin": "https://github.com/brunocodutra/webapp-webpack-plugin.git", "webpack": "^4.19.1", "webpack-bundle-analyzer": "^3.0.2", "webpack-cli": "^3.1.1", "webpack-dashboard": "^2.0.0", "webpack-dev-server": "^3.1.9", "webpack-manifest-plugin": "^2.0.4", "webpack-merge": "^4.1.4", "webpack-notifier": "^1.6.0", "workbox-webpack-plugin": "^3.6.2"
},複製程式碼

沒錯,這裡面依賴了很多npm包,但我們的構建過程確實做的事情需要用到它們。

最後,dependencies的使用:

"dependencies": { 
"@babel/polyfill": "^7.0.0", "axios": "^0.18.0", "tailwindcss": "^0.6.6", "vue": "^2.5.17", "vue-confetti": "^0.4.2"
}
}複製程式碼

顯然,對於一個真實存在的網站或者應用,dependencies中會有更多npm包,但我們現在專注於構建過程。

ANNOTATED WEBPACK.SETTINGS.JS

我還使用了我在 A Bet­ter package.json for the Fron­tend arti­cle一文中討論過的類似方法,為了封鎖從專案之間配置變為單獨的webpack.settings.js,並保持webpack配置本身不變。

The key concept is that the only file we need to edit from project to project is the webpack.settings.js. [關鍵概念是我們需要在專案之間編輯的唯一檔案是webpack.settings.js]

由於大部分專案都有一些非常相似的事情需要完成,所以我們可以建立一個適用於各個專案的webpack配置,我們只需要更改它所操作的資料。

因此,在我們的webpack.settings.js配置檔案中的內容(從專案到專案的資料)和webpack配置中的內容(如何操作這些資料產生最終結果)之間的關注點分離。

// webpack.settings.js - webpack settings config// node modulesrequire('dotenv').config();
// Webpack settings exports// noinspection WebpackConfigHighlightingmodule.exports = {
name: "Example Project", copyright: "Example Company, Inc.", paths: {
src: {
base: "./src/", css: "./src/css/", js: "./src/js/"
}, dist: {
base: "./web/dist/", clean: [ "./img", "./criticalcss", "./css", "./js" ]
}, templates: "./templates/"
}, urls: {
live: "https://example.com/", local: "http://example.test/", critical: "http://example.test/", publicPath: "/dist/"
}, vars: {
cssName: "styles"
}, entries: {
"app": "app.js"
}, copyWebpackConfig: [ {
from: "./src/js/workbox-catch-handler.js", to: "js/[name].[ext]"
} ], criticalCssConfig: {
base: "./web/dist/criticalcss/", suffix: "_critical.min.css", criticalHeight: 1200, criticalWidth: 1200, ampPrefix: "amp_", ampCriticalHeight: 19200, ampCriticalWidth: 600, pages: [ {
url: "", template: "index"
} ]
}, devServerConfig: {
public: () =>
process.env.DEVSERVER_PUBLIC || "http://localhost:8080", host: () =>
process.env.DEVSERVER_HOST || "localhost", poll: () =>
process.env.DEVSERVER_POLL || false, port: () =>
process.env.DEVSERVER_PORT || 8080, https: () =>
process.env.DEVSERVER_HTTPS || false,
}, manifestConfig: {
basePath: ""
}, purgeCssConfig: {
paths: [ "./templates/**/*.{twig,html
}"
, "./src/vue/**/*.{vue,html
}"
], whitelist: [ "./src/css/components/**/*.{css,pcss
}"
], whitelistPatterns: [], extensions: [ "html", "js", "twig", "vue" ]
}, saveRemoteFileConfig: [ {
url: "https://www.google-analytics.com/analytics.js", filepath: "js/analytics.js"
} ], createSymlinkConfig: [ {
origin: "img/favicons/favicon.ico", symlink: "../favicon.ico"
} ], webappConfig: {
logo: "./src/img/favicon-src.png", prefix: "img/favicons/"
}, workboxConfig: {
swDest: "../sw.js", precacheManifestFilename: "js/precache-manifest.[manifestHash].js", importScripts: [ "/dist/workbox-catch-handler.js" ], exclude: [ /\.(png|jpe?g|gif|svg|webp)$/i, /\.map$/, /^manifest.*\\.js(?:on)?$/, ], globDirectory: "./web/", globPatterns: [ "offline.html", "offline.svg" ], offlineGoogleAnalytics: true, runtimeCaching: [ {
urlPattern: /\.(?:png|jpg|jpeg|svg|webp)$/, handler: "cacheFirst", options: {
cacheName: "images", expiration: {
maxEntries: 20
}
}
} ]
}
};
複製程式碼

我們將在webpack配置部分介紹所有內容,這裡需要注意的重點是,我們已經採取了從專案到專案的更改,並加其從我們的webpack配置檔案中分離出來,並新增到單獨的webpack.settings.js檔案中。

這意味著我們可以在webpack.settings.js配置檔案中定義每個專案不同的地方,而不需要與webpack本身配置進行摻和在一起。儘管webpack.settings.js檔案是一個js檔案,但我儘量將它保持為JSON-ish,所以我們只是更改其中的簡單設定,我沒有使用JSON作為檔案格式的靈活性,也允許新增註釋。

COMMON CONVENTIONS FOR WEBPACK CONFIGS

我為所有webpack配置檔案(webpack.common.jswebpack.dev.jswebpack.prod.js)採用了一些約定,讓它們看起來比較一致。

每個配置檔案都有兩個內建配置:

  • legacyConfig —— 適用於舊版ES5構建的配置

  • modernConfig —— 適用於構建現代ES2015+版本的配置

我們這樣做是因為我們有單獨的配置來建立相容舊版本與現代構建,使它們在邏輯獨立。webpack.common.js 也有一個baseConfig,為了保證組織的純粹。

可以把它想象成物件導向程式設計,其中各種配置專案繼承,baseConfig作為根物件。

為了保證配置簡潔清晰和具有可讀性,採用的另一個約定是為各種webpack外掛和需要配置的其他webpack片段配置configure()函式,而不是全部混在一起。

這樣做是因為在webpack.settings.js中的一些資料需要在使用webpack之前進行轉換,並且由於過去/現代構建,我們需要根據構建型別返回不同的配置。

它還使配置檔案更具可讀性。

作為一個通用的webpack概念,要知道webpack本身只知道如何載入JavaScript和JSON。要載入其他東西,需要使用對應的載入器,我們將在webpack配置中使用許多不同的載入器。

ANNOTATED WEBPACK.COMMON.JS

現在讓我們看一下webpack.common.js配置檔案,包含devprod構建型別間共享的所有配置。

// webpack.common.js - common webpack configconst LEGACY_CONFIG = 'legacy';
const MODERN_CONFIG = 'modern';
// node modulesconst path = require('path');
const merge = require('webpack-merge');
// webpack pluginsconst CopyWebpackPlugin = require('copy-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const WebpackNotifierPlugin = require('webpack-notifier');
// config filesconst pkg = require('./package.json');
const settings = require('./webpack.settings.js');
複製程式碼

在一開始,我們引入了我們需要的node包,以及需要使用的webpack外掛。然後我們匯入webpack.settings.js 作為settings,以便我們可以訪問那裡的設定,並將package.json作為pkg匯入,對其進行訪問。

CONFIGURATION FUNCTIONS

這是configureBabelLoader()的設定:

// Configure Babel loaderconst configureBabelLoader = (browserList) =>
{
return {
test: /\.js$/, exclude: /node_modules/, use: {
loader: 'babel-loader', options: {
presets: [ [ '@babel/preset-env', {
modules: false, useBuiltIns: 'entry', targets: {
browsers: browserList,
},
} ], ], plugins: [ '@babel/plugin-syntax-dynamic-import', [ "@babel/plugin-transform-runtime", {
"regenerator": true
} ] ],
},
},
};

};
複製程式碼

函式configureBabelLoader()配置babel-loader來處理所有js字尾檔案的載入,它使用@babel/preset-env而不是.babelrc檔案,因此我們可以把所以內容保留在webpack配置檔案中。

Babel可以將現代ES2015+(以及其他許多語言,如TypeScript或CoffeeScript)編譯為針對特定瀏覽器或標準的JavaScript。我們將browserList作為引數傳入,這樣我們可以為舊版瀏覽器構建現代ES2015+模組和用polyfills相容舊版ES5。

在我們的HTML中,我們只做這樣的事情:

<
!-- Browsers with ES module support load this file. -->
<
script type="module" src="main.js">
<
/script>
<
!-- Older browsers load this file (and module-supporting -->
<
!-- browsers know *not* to load this file). -->
<
script nomodule src="main-legacy.js">
<
/script>
複製程式碼

不用polyfills,不用大驚小怪,舊版瀏覽器忽略type="module"指令碼,並獲取main-legacy.js,新版瀏覽器載入main.js,忽略nomodule,看起來很棒,真慶幸我想出了這個想法!為了不讓你覺得這種方法是極端,vue-cli在版本3中採用了這種策略

@ babel/plugin-syntax-dynamic-import外掛甚至可以在web瀏覽器實現ECMAScripr動態匯入之前進行動態匯入,這使我們可以非同步載入我們的JavaScript模組,並根據需要動態載入

那麼到底在說啥?這意味著我們可以做這樣的事:

// App mainconst main = async () =>
{
// Async load the vue module const Vue = await import(/* webpackChunkName: "vue" */ 'vue');
// Create our vue instance const vm = new Vue.default({
el: "#app", components: {
'confetti': () =>
import(/* webpackChunkName: "confetti" */ '../vue/Confetti.vue'),
},
});

};
// Execute async functionmain().then( (value) =>
{
});
複製程式碼

有兩點:

1、通過/* webpackChunkName: "vue" */,我們告訴webpack希望這個動態程式碼拆分塊被命名。

2、由於我們在非同步函式(“main”)中使用import(),該函式等待動態載入的JavaScript匯入的結果,而其餘的程式碼以其方式繼續。

我們已經有效地告訴webpack,我們希望我們的塊通過程式碼分割,而不是通過配置,通過@babel/plugin-syntax-dynamic-import的自帶魔法,可以根據需要非同步載入此JavaScript塊。

注意,我們也是使用.vue單檔案元件做了同樣的操作,很好。

除了使用await,我們也可以在import()Promise返回後執行我們的程式碼:

// Async load the vue moduleimport(/* webpackChunkName: "vue" */ 'vue').then(Vue =>
{
// Vue has loaded, do something with it // Create our vue instance const vm = new Vue.default({
el: "#app", components: {
'confetti': () =>
import(/* webpackChunkName: "confetti" */ '../vue/Confetti.vue'),
},
});

});
複製程式碼

這裡我們使用了Promise,而不是await,因此我們知道動態匯入已經成功並且可以愉快地使用Vue

如果你足夠仔細,你可以看到我們通過Promises有效地解決了JavaScript依賴關係,太棒了!

我們甚至可以在使用者點選了某些內容,滾動到某個位置或者滿足其他條件後去載入某些JavaScript快等有趣的事情。

檢視更多關於Module Methods import()資訊。

如果你有興趣瞭解更多有關Babel的資訊,可以檢視Working with Babel 7 and Webpack這篇文章。

接下來我們有configureEntries()

// Configure Entriesconst configureEntries = () =>
{
let entries = {
};
for (const [key, value] of Object.entries(settings.entries)) {
entries[key] = path.resolve(__dirname, settings.paths.src.js + value);

} return entries;

};
複製程式碼

這裡我們通過swttings.entrieswebpack.settings.js中拿到webpack entry,對於單頁應用(SPA),只存在一個entry。對於更傳統的網站,你可能有幾個entry(每頁模版可能有一個entry)。

無論哪種方式,由於我們已經在webpack.settings.js中定義了entry points,所以很容易在檔案對其進行配置,entry points實際上只是一個<
script src =“app.js”>
<
/ script>
標記,你將在HTML中包含該標記以引入JavaScript。

由於我們使用的是動態匯入模組,因此我們通常在頁面上只有一個<
script>
<
/script>
標籤;其餘的JavaScript會根據需要動態載入。

接下來我們有configureFontLoader()函式:

// Configure Font loaderconst configureFontLoader = () =>
{
return {
test: /\.(ttf|eot|woff2?)$/i, use: [ {
loader: 'file-loader', options: {
name: 'fonts/[name].[ext]'
}
} ]
};

};
複製程式碼

devprod構建字型載入是相同的,所以我們把它寫在這裡,對於我們使用的任何本地字型,我們可以通知webpack在JavaScript中載入它們:

import comicsans from '../fonts/ComicSans.woff2';
複製程式碼

接下來我們有configureManifest()函式:

// Configure Manifestconst configureManifest = (fileName) =>
{
return {
fileName: fileName, basePath: settings.manifestConfig.basePath, map: (file) =>
{
file.name = file.name.replace(/(\.[a-f0-9]{32
})(\..*)$/, '$2');
return file;

},
};

};
複製程式碼

這會為基於檔名的快取清除配置webpack-manifest-plugin,簡單來說,webpack知道我們需要的所有JavaScript、css和其他資源,所以它可以生成一個指向帶雜湊命名的資源清單,例如:

{ 
"vendors~confetti~vue.js": "/dist/js/vendors~confetti~vue.03b9213ce186db5518ea.js", "vendors~confetti~vue.js.map": "/dist/js/vendors~confetti~vue.03b9213ce186db5518ea.js.map", "app.js": "/dist/js/app.30334b5124fa6e221464.js", "app.js.map": "/dist/js/app.30334b5124fa6e221464.js.map", "confetti.js": "/dist/js/confetti.1152197f8c58a1b40b34.js", "confetti.js.map": "/dist/js/confetti.1152197f8c58a1b40b34.js.map", "js/precache-manifest.js": "/dist/js/precache-manifest.f774c437974257fc8026ca1bc693655c.js", "../sw.js": "/dist/../sw.js"
}複製程式碼

我們傳入檔名,因為建立一個現代的monifest.json以及一個用於相容的manifest-legacy.json,它們分別具有現代ES2015+模組和相容舊版ES5模組的入口點。對於為現代以及舊版本生成的資源,這兩個json檔案中的關鍵點都是一致的。

接下來我們有一個相當標準的configureVueLoader()配置:

// Configure Vue loaderconst configureVueLoader = () =>
{
return {
test: /\.vue$/, loader: 'vue-loader'
};

};
複製程式碼

這配置只是讓我們輕鬆解析Vue單檔案元件,webpack負責為你提取適當的HTML、CSS和Javascript。

BASE CONFIG

baseConfig將與modernConfiglegacyConfig合併:

// The base webpack configconst baseConfig = { 
name: pkg.name, entry: configureEntries(), output: {
path: path.resolve(__dirname, settings.paths.dist.base), publicPath: settings.urls.publicPath
}, resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
}, module: {
rules: [ configureVueLoader(), ],
}, plugins: [ new WebpackNotifierPlugin({title: 'Webpack', excludeWarnings: true, alwaysNotify: true
}), new VueLoaderPlugin(), ]
};
複製程式碼

這裡所有的配置都是非常標準的webpack配置,但請注意我們將vue$指向vue/dist/vue.esm.js,以便我們可以獲得Vue的ES2015模組版本。

我們使用WebpackNotifierPlugin外掛以直觀的方式告訴我們構建的狀態。

LEGACY CONFIG

legacyConfig配置用於使用合適的polyfill構建相容舊版本ES5:

// Legacy webpack configconst legacyConfig = { 
module: {
rules: [ configureBabelLoader(Object.values(pkg.browserslist.legacyBrowsers)), ],
}, plugins: [ new CopyWebpackPlugin( settings.copyWebpackConfig ), new ManifestPlugin( configureManifest('manifest-legacy.json') ), ]
};
複製程式碼

請注意,我們將pkg.browserslist.legacyBrowsers傳遞給configureBabelLoader(),將manifest-legacy.json傳遞給configureManifest()

我們還在此配置中加入了CopyWebpackPlugin外掛,我們只需要複製settings.copyWebpackConfig中定義的檔案一次。

MODERN CONFIG

modernConfig用於構建現代ES2015 Javascript模組,不需要藉助其他東西:

// Modern webpack configconst modernConfig = { 
module: {
rules: [ configureBabelLoader(Object.values(pkg.browserslist.modernBrowsers)), ],
}, plugins: [ new ManifestPlugin( configureManifest('manifest.json') ), ]
};
複製程式碼

請注意,我們將pkg.browserslist.modernBrowsers傳遞給configureBabelLoader(),將manifest.json傳遞給configureManifest()

MODULE.EXPORTS

最後,module.exports使用webpack-merge外掛將之前的配置合併在一起,並返回webpack.dev.jswebpack.prod.js使用的物件。

// Common module exports// noinspection WebpackConfigHighlightingmodule.exports = { 
'legacyConfig': merge( legacyConfig, baseConfig, ), 'modernConfig': merge( modernConfig, baseConfig, ),
};
複製程式碼

ANNOTATED WEBPACK.DEV.JS

現在讓我們看看webpack.dev.js配置檔案,它包含了我們開發專案時用於構建的所有設定,與webpack.common.js檔案中的設定合併,形成一個完整的webpack配置。

// webpack.dev.js - developmental buildsconst LEGACY_CONFIG = 'legacy';
const MODERN_CONFIG = 'modern';
// node modulesconst merge = require('webpack-merge');
const path = require('path');
const sane = require('sane');
const webpack = require('webpack');
// webpack pluginsconst Dashboard = require('webpack-dashboard');
const DashboardPlugin = require('webpack-dashboard/plugin');
const dashboard = new Dashboard();
// config filesconst common = require('./webpack.common.js');
const pkg = require('./package.json');
const settings = require('./webpack.settings.js');
複製程式碼

在序言中,我們再次引入了需要用到的node包,以及使用的webpack外掛,然後引入webpack.settings.js作為settings,以便我們可以訪問那裡的設定,並匯入package.json作為pkg,以便訪問那裡的一些設定。

我們同時還匯入了webpack.common.js常用的webpack配置,並將合併到我們的開發設定。

CONFIGURATION FUNCTIONS

這是configureDevServer()的配置:

// Configure the webpack-dev-serverconst configureDevServer = (buildType) =>
{
return {
public: settings.devServerConfig.public(), contentBase: path.resolve(__dirname, settings.paths.templates), host: settings.devServerConfig.host(), port: settings.devServerConfig.port(), https: !!parseInt(settings.devServerConfig.https()), quiet: true, hot: true, hotOnly: true, overlay: true, stats: 'errors-only', watchOptions: {
poll: !!parseInt(settings.devServerConfig.poll()), ignored: /node_modules/,
}, headers: {
'Access-Control-Allow-Origin': '*'
}, // Use sane to monitor all of the templates files and sub-directories before: (app, server) =>
{
const watcher = sane(path.join(__dirname, settings.paths.templates), {
glob: ['**/*'], poll: !!parseInt(settings.devServerConfig.poll()),
});
watcher.on('change', function(filePath, root, stat) {
console.log(' File modified:', filePath);
server.sockWrite(server.sockets, "content-changed");

});

},
};

};
複製程式碼

當我們進行生產構建時,webpack繫結所有各種資源並儲存到檔案系統中,相比之下,當我們在本地專案中開發時,我們通過webpack-dev-server使用開發構建:

  • 啟動為我們的資源提供服務的本地express web伺服器。

  • 為了提升速度,在記憶體而不是檔案系統中構建我們的資源。

  • 重新構建資源,如JavaScript、css、Vue元件等等,通過使用熱模組更新(HMR),當我們修改了這些資源,可以不需要重新載入介面。

  • 在更改模版時將會重新載入頁面。

這類似於更復雜的Browsersync變體,大大加快了開發速度。

唯一不同的是我們這裡使用了Sane監控不需要通過webpack執行的檔案(本例中我們的模板),當該檔案修改時,重新載入頁面。

注意,webpack-dev-server的配置再次引用了webpack.settings.js檔案,對於大部分人來說預設值可能沒問題,但我使用Laravel Homestead作為本地開發,像我們在文章Local Development with Vagrant / Homestead討論的那樣,意味著我在Homestead VM中執行所有的開發工具。

因此,webpack.settings.js可以從一個.env檔案中讀取擁有特定的devServer配置,而不是在我的weboack.settings.js檔案中對本地開發環境進行硬編碼(因為它可能因人而異):

// .env file DEVSERVER settings# webpack example settings for Homestead/VagrantDEVSERVER_PUBLIC="http://192.168.10.10:8080"DEVSERVER_HOST="0.0.0.0"DEVSERVER_POLL=1DEVSERVER_PORT=8080DEVSERVER_HTTPS=0複製程式碼

你可以使用不同的配置,因此可以根據需要在.env檔案中更改設定,dotenv背後的想法是我們在.env檔案中定義了一個特定於環境的配置,不會將其簽入git repo。如果.env檔案不存在,那很好,使用預設值:

// webpack.settings.js devServerConfig defaultsdevServerConfig: { 
public: () =>
process.env.DEVSERVER_PUBLIC || "http://localhost:8080", host: () =>
process.env.DEVSERVER_HOST || "localhost", poll: () =>
process.env.DEVSERVER_POLL || false, port: () =>
process.env.DEVSERVER_PORT || 8080, https: () =>
process.env.DEVSERVER_HTTPS || false,
},複製程式碼

接下來是configureImageLoader()配置:

// webpack.dev.js configureImageLoader()// Configure Image loaderconst configureImageLoader = (buildType) =>
{
if (buildType === LEGACY_CONFIG) {
return {
test: /\.(png|jpe?g|gif|svg|webp)$/i, use: [ {
loader: 'file-loader', options: {
name: 'img/[name].[hash].[ext]'
}
} ]
};

} if (buildType === MODERN_CONFIG) {
return {
test: /\.(png|jpe?g|gif|svg|webp)$/i, use: [ {
loader: 'file-loader', options: {
name: 'img/[name].[hash].[ext]'
}
} ]
};

}
};
複製程式碼

傳入buildType引數,以便返回不同的結果,具體取決於它是舊版本還是新版構建,在該例子中,我們返回了相同的配置,但可以想象可能會改變。

值得注意的是,這只是適用於我們webpack構建中包含的圖片;許多其他的圖片來自於其他地方(CMS系統,資產管理系統等等)。

要讓webpack知道這裡有影像,需要將其匯入到你的JavaScript檔案中:

import Icon from './icon.png';
複製程式碼

有關這方面的更多詳細資訊,請檢視webpack文件“載入影像”部分。

接下來是configurePostcssLoader()配置:

// Configure the Postcss loaderconst configurePostcssLoader = (buildType) =>
{
// Don't generate CSS for the legacy config in development if (buildType === LEGACY_CONFIG) {
return {
test: /\.(pcss|css)$/, loader: '
ignore-loader'
};

} if (buildType === MODERN_CONFIG) {
return {
test: /\.(pcss|css)$/, use: [ {
loader: '
style-loader',
}, {
loader: '
vue-style-loader',
}, {
loader: '
css-loader', options: {
importLoaders: 2, sourceMap: true
}
}, {
loader: '
resolve-url-loader'
}, {
loader: '
postcss-loader', options: {
sourceMap: true
}
} ]
};

}
};
複製程式碼

我們使用PostCSS來處理所有的css,包括Tailwind CSS。我覺得PostCSS是css的Babel,它將各種高階css功能程式設計成瀏覽器可以解析的普通css。

對於webpack載入器,它們的處理順序與列出順序相反:

我們在本地開發過程中不需要將所有css檔案提取到最小的檔案中,相反我們只是讓style-loader在我們的文件中內聯它。

webpack-dev-server為css使用熱模組替換(HMR),每當我們修改樣式時,它都會重新構建css並自動注入,很神奇(what)。

我們通過引入它來告知webpack去解析:

import styles from '../css/app.pcss';
複製程式碼

在webpack文件的Loading CSS部分中有詳細討論。

我們從App.js入口點執行此操作,將此視為PostCSS的入口點,app.pcss檔案@import我們專案中使用到的所有CSS,後面會對此進行詳細介紹。

MODULE.EXPORTS

最後,module.exports使用webpack-merge包將webpack.common.js中的common.legacyConfig與我們的開發舊版相容配置合併,並將common.modernConfig與開發環境現代配置合併:

// Development module exportsmodule.exports = [    merge(        common.legacyConfig,        { 
output: {
filename: path.join('./js', '[name]-legacy.[hash].js'), publicPath: settings.devServerConfig.public() + '/',
}, mode: 'development', devtool: 'inline-source-map', devServer: configureDevServer(LEGACY_CONFIG), module: {
rules: [ configurePostcssLoader(LEGACY_CONFIG), configureImageLoader(LEGACY_CONFIG), ],
}, plugins: [ new webpack.HotModuleReplacementPlugin(), ],
} ), merge( common.modernConfig, {
output: {
filename: path.join('./js', '[name].[hash].js'), publicPath: settings.devServerConfig.public() + '/',
}, mode: 'development', devtool: 'inline-source-map', devServer: configureDevServer(MODERN_CONFIG), module: {
rules: [ configurePostcssLoader(MODERN_CONFIG), configureImageLoader(MODERN_CONFIG), ],
}, plugins: [ new webpack.HotModuleReplacementPlugin(), new DashboardPlugin(dashboard.setData), ],
} ),];
複製程式碼

通過module.exports中返回一個陣列,我們告知webpack有多個需要執行的編譯:一個用於舊版相容構建,另一個用於新版構建。

對於舊版構建,我們將處理後的JavaScript命名為[name]-legacy.[hash].js,而新版構建命名為[name].[hash].js

通過設定modedevelopment,告知webpack這是開發環境構建。

devtool設定為inline-source-map,我們要求將CSS/JavsScript的.map內聯到檔案中,雖然構建出來的專案會偏大,但是便於開發除錯。

通過webpack.HotModuleReplacementPlugin外掛,可以支援Webpack的熱模組替換(HMR)。

DashboardPlugin外掛讓我們覺得自己是一個宇航員,擁有一個酷炫的皮膚:

用於前端開發的webpack4配置[帶註釋]

我發現DashboardPlugin外掛開發HUD比預設的webpack進度展示更直觀。

到這裡,現在已經為我們專案提供了一個很好的開發環境配置,檢視熱模組替換視訊,瞭解該操作的示例

ANNOTATED WEBPACK.PROD.JS

現在我們看看webpack.prod.js配置檔案,它包含我們正在處理專案時用於生產構建的所有配置。它與webpack.common.js中的設定合併,形成一個完整的webpack配置。

// webpack.prod.js - production buildsconst LEGACY_CONFIG = 'legacy';
const MODERN_CONFIG = 'modern';
// node modulesconst git = require('git-rev-sync');
const glob = require('glob-all');
const merge = require('webpack-merge');
const moment = require('moment');
const path = require('path');
const webpack = require('webpack');
// webpack pluginsconst BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CreateSymlinkPlugin = require('create-symlink-webpack-plugin');
const CriticalCssPlugin = require('critical-css-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ImageminWebpWebpackPlugin = require('imagemin-webp-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const PurgecssPlugin = require('purgecss-webpack-plugin');
const SaveRemoteFilePlugin = require('save-remote-file-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const WebappWebpackPlugin = require('webapp-webpack-plugin');
const WhitelisterPlugin = require('purgecss-whitelister');
const WorkboxPlugin = require('workbox-webpack-plugin');
// config filesconst common = require('./webpack.common.js');
const pkg = require('./package.json');
const settings = require('./webpack.settings.js');
複製程式碼

我們再次引入了在序言中涉及到的node包,以及我們使用的webpack外掛,然後將webpack.settings.js作為settings匯入,並將package.json作為pkg匯入,便於訪問需要用到的配置。

我們還匯入了webpack.common.js中公共的webpack配置,我們將與開發設定合併。

TAILWIND EXTRACTOR

該類是Tailwind CSS的自定義PurgeCSS提取器,允許在類名中使用特殊字元。

// Custom PurgeCSS extractor for Tailwind that allows special characters in// class names.//// https://github.com/FullHuman/purgecss#extractorclass TailwindExtractor { 
static extract(content) {
return content.match(/[A-Za-z0-9-_:\/]+/g) || [];

}
}複製程式碼

這取自Tailwind CSS文件中Removing unused CSS with PurgeCSS這一部分。有關此提取器如何與 purgcss 配合使用的詳細資訊, 請參閱下文, 讓你的css變得更加的整潔。

CONFIGURATION FUNCTIONS

這是configureBanner()函式:

// Configure file bannerconst configureBanner = () =>
{
return {
banner: [ '/*!', ' * @project ' + settings.name, ' * @name ' + '[filebase]', ' * @author ' + pkg.author.name, ' * @build ' + moment().format('llll') + ' ET', ' * @release ' + git.long() + ' [' + git.branch() + ']', ' * @copyright Copyright (c) ' + moment().format('YYYY') + ' ' + settings.copyright, ' *', ' */', '' ].join('\n'), raw: true
};

};
複製程式碼

這只是為我們生成的每個檔案新增了一個帶有專案名稱、檔名、作者和 git 資訊的banner。

接著是configureBundleAnalyzer()

// webpack.prod.js configureBundleAnalyzer()// Configure Bundle Analyzerconst configureBundleAnalyzer = (buildType) =>
{
if (buildType === LEGACY_CONFIG) {
return {
analyzerMode: 'static', reportFilename: 'report-legacy.html',
};

} if (buildType === MODERN_CONFIG) {
return {
analyzerMode: 'static', reportFilename: 'report-modern.html',
};

}
};
複製程式碼

使用 WebpackBundleAnalyzer 外掛為我們的新版和舊版本構建生成一份報告,並且生成一個獨立可互動的HTML頁面,可以檢視webpack打包後的確切內容。

用於前端開發的webpack4配置[帶註釋]

我發現這個外掛挺有用,可以幫助我縮小最終構建包的大小,而且確切地瞭解了webpack構建了什麼,所以我已經把它作為專案生產構建過程的一部分。

接著是configureCriticalCss()

// webpack.prod.js configureCriticalCss()// Configure Critical CSSconst configureCriticalCss = () =>
{
return (settings.criticalCssConfig.pages.map((row) =>
{
const criticalSrc = settings.urls.critical + row.url;
const criticalDest = settings.criticalCssConfig.base + row.template + settings.criticalCssConfig.suffix;
let criticalWidth = settings.criticalCssConfig.criticalWidth;
let criticalHeight = settings.criticalCssConfig.criticalHeight;
// Handle Google AMP templates if (row.template.indexOf(settings.criticalCssConfig.ampPrefix) !== -1) {
criticalWidth = settings.criticalCssConfig.ampCriticalWidth;
criticalHeight = settings.criticalCssConfig.ampCriticalHeight;

} console.log("source: " + criticalSrc + " dest: " + criticalDest);
return new CriticalCssPlugin({
base: './', src: criticalSrc, dest: criticalDest, extract: false, inline: false, minify: true, width: criticalWidth, height: criticalHeight,
})
}) );

};
複製程式碼

使用CriticalCssPlugin外掛通過webpack.settings.js中的settings.criticalCssConfig.pages進行分塊,為我們的網站生成CriticalCSS。

需要注意的是,如果傳入的頁面在任何位置的名字都包含settings.criticalCssConfig.ampPrefix,則它將通過傳入非常大的高度為整個網頁(而不僅僅是上面的摺疊內容)生成CriticalCSS。

這裡不會詳細介紹CriticalCSS,有關它的更多資料,請檢視Implementing Critical CSS on your website這篇文章。

接著是configureCleanWebpack()

// Configure Clean webpackconst configureCleanWebpack = () =>
{
return {
root: path.resolve(__dirname, settings.paths.dist.base), verbose: true, dry: false
};

};
複製程式碼

這只是使用CleanWebpackPlugin從我們的webpack.settings.js中刪除 settings.paths.dist.base 中的生成目錄。

接著是configureHtml()

// Configure Html webpackconst configureHtml = () =>
{
return {
templateContent: '', filename: 'webapp.html', inject: false,
};

};
複製程式碼

這將使用HtmlWebpackPluginWebappWebpackPlugin(見下文)外掛為我們的favicons生成HTML。注意,我們在templateContent中傳入一個空字串,以便輸出只是WebappWebpackPlugin的原始輸出。

接著是configureImageLoader()

// Configure Image loaderconst configureImageLoader = (buildType) =>
{
if (buildType === LEGACY_CONFIG) {
return {
test: /\.(png|jpe?g|gif|svg|webp)$/i, use: [ {
loader: 'file-loader', options: {
name: 'img/[name].[hash].[ext]'
}
} ]
};

} if (buildType === MODERN_CONFIG) {
return {
test: /\.(png|jpe?g|gif|svg|webp)$/i, use: [ {
loader: 'file-loader', options: {
name: 'img/[name].[hash].[ext]'
}
}, {
loader: 'img-loader', options: {
plugins: [ require('imagemin-gifsicle')({
interlaced: true,
}), require('imagemin-mozjpeg')({
progressive: true, arithmetic: false,
}), require('imagemin-optipng')({
optimizationLevel: 5,
}), require('imagemin-svgo')({
plugins: [ {convertPathData: false
}, ]
}), ]
}
} ]
};

}
};
複製程式碼

我們傳入buildType引數,以至於我們可以返回不同的結果,具體取決於它是新版還是舊版構建。我們通過優化處理影像,通過img-loader進行新版構建。

我們只對新版構建執行此操作,因為花費時間去處理優化新版本和舊版本的影像沒有意義(影像對於兩者都是一樣的)。

需要注意的是,這隻適用於我們的webpack構建中包含的影像,許多其他影像資源其實來自來與其他地方(cms 系統、資產管理系統等)。

要讓webpack優化影像,請將其匯入 JavaScript:

import Icon from './icon.png';
複製程式碼

更多Loading Images詳細資訊,請檢視webpack文件對應部分。

接著是我們的configureOptimization()配置:

// Configure optimizationconst configureOptimization = (buildType) =>
{
if (buildType === LEGACY_CONFIG) {
return {
splitChunks: {
cacheGroups: {
default: false, common: false, styles: {
name: settings.vars.cssName, test: /\.(pcss|css|vue)$/, chunks: 'all', enforce: true
}
}
}, minimizer: [ new TerserPlugin( configureTerser() ), new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
map: {
inline: false, annotation: true,
}, safe: true, discardComments: true
},
}) ]
};

} if (buildType === MODERN_CONFIG) {
return {
minimizer: [ new TerserPlugin( configureTerser() ), ]
};

}
};
複製程式碼

這是webpack生產環境優化的配置,對於舊版構建(執行此操作兩次沒有任何意義),我們使用MiniCssExtractPlugin外掛將專案裡使用到的css提取到單個css檔案中。如果您以前使用過webpack,那麼以前應該已經使用過ExtractTextPlugin來執行過此操作,然而現在不需要這麼做了。

我們還使用了OptimizeCSSAssetsPlugin外掛通過刪除重複的規則來優化生成的css,並通過cssnano壓縮css。

最後,我們將Javascript minimizer設定成TerserPlugin,這是因為[UglifyJsPlugin](github.com/webpack-con…)不再支援最小化ES2015+JavaScript。由於我們正在生成新版es2015+bundles,我們需要它。

接著是configurePostcssLoader()

// Configure Postcss loaderconst configurePostcssLoader = (buildType) =>
{
if (buildType === LEGACY_CONFIG) {
return {
test: /\.(pcss|css)$/, use: [ MiniCssExtractPlugin.loader, {
loader: 'css-loader', options: {
importLoaders: 2, sourceMap: true
}
}, {
loader: 'resolve-url-loader'
}, {
loader: 'postcss-loader', options: {
sourceMap: true
}
} ]
};

} // Don't generate CSS for the modern config in production if (buildType === MODERN_CONFIG) {
return {
test: /\.(pcss|css)$/, loader: '
ignore-loader'
};

}
};
複製程式碼

這個配置看起來十分類似於開發版本的configurePostcssLoader(),除了最終載入器,我們使用MiniCssExtractPlugin.loader將所有css提取到一個檔案中。

我們只對舊版相容構建執行此操作,因為對每個構建執行它沒有意義(css是相同的)。我們使用ignore-loader進行新版構建,因此我們的.css和.pcss檔案存在一個載入器,但什麼都沒做。

如前面說到,我們使用PostCSS處理所有的css,包括Tailwind CSS,我認為它是CSS的babel,因為它將各種高階的css功能編譯成你的瀏覽器可以解析的普通css。

同樣,對於webpack載入器,它們按照列出的相反順序進行處理:

由於這是一個生產環境構建,我們使用MiniCssExtractPlugin.loader提取所有使用到的css,並儲存到.css檔案中。CSS也被最小化,並針對生產環境進行了優化。

我們通過引入css檔案告知webpack:

import styles from '../css/app.pcss';
複製程式碼

這在webpack文件的Loading CSS有詳細介紹。

我們從App.js入口點執行此操作,將此視為postCSS的入口點,app.pcss檔案@import我們專案使用的所有CSS,稍後將詳細介紹。

接著是configurePurgeCss()

// Configure PurgeCSSconst configurePurgeCss = () =>
{
let paths = [];
// Configure whitelist paths for (const [key, value] of Object.entries(settings.purgeCssConfig.paths)) {
paths.push(path.join(__dirname, value));

} return {
paths: glob.sync(paths), whitelist: WhitelisterPlugin(settings.purgeCssConfig.whitelist), whitelistPatterns: settings.purgeCssConfig.whitelistPatterns, extractors: [ {
extractor: TailwindExtractor, extensions: settings.purgeCssConfig.extensions
} ]
};

};
複製程式碼

Tailwind CSS是一個出色的實用程式優先的CSS框架,它允許快速原型化,因為在本地開發中,很少需要實際編寫任何css。 相反,你只需要使用提供的實用程式CSS類。

缺點就是生成的CSS可能有點大,這時候就需要用到PurgeCSS,它將解析所有HTML/template/Vue/任何檔案,並刪除沒有使用到的CSS。

節省的空間可能很大,Tailwind CSS和PurgeCSS是天作之合。我們在 Tailwind CSS utility-first CSS with Adam Wathan部落格中深入討論了這個問題。

它遍歷settings.purgeCssConfig.paths中的所有路徑globs,以尋找要保留的CSS規則,任何未找到的CSS規則都會從我們生成的CSS構建中刪除。

我們還使用了WhitelisterPlugin,當我們知道不希望某些CSS被剝離時,可以輕鬆地將整個檔案或全域性列入白名單。與我們的settings.purgeCssConfig.whitelist匹配的所有檔案中的CSS規則都列入白名單,並且永遠不會從生成的構建中刪除。

接下來是configureTerser()

// Configure terserconst configureTerser = () =>
{
return {
cache: true, parallel: true, sourceMap: true
};

};
複製程式碼

這只是配置了[TerserPlugin](github.com/webpack-con…)使用的一些設定,最大限度地減少了我們的舊版和新版JavaScript程式碼。

接著是configureWebApp()

// Configure Webapp webpackconst configureWebapp = () =>
{
return {
logo: settings.webappConfig.logo, prefix: settings.webappConfig.prefix, cache: false, inject: 'force', favicons: {
appName: pkg.name, appDescription: pkg.description, developerName: pkg.author.name, developerURL: pkg.author.url, path: settings.paths.dist.base,
}
};

};
複製程式碼

這裡使用webappwebpackepulin以無數種格式生成我們所有的網站favicon,以及我們的webapp manifest.json和其他PWA細節。

它與HtmlWebpackPlugin結合使用,還可以輸出一個webapp.html檔案,它包含所有生成的favicons和相關檔案的連結,以包含在我們的HTML頁面的<
head>
<
/head>
中。

接著是configureWorkbox()

// Configure Workbox service workerconst configureWorkbox = () =>
{
let config = settings.workboxConfig;
return config;

};
複製程式碼

我們使用Google的WorkboxWebpackPlugin為網站生成一個Service Worker,解釋 Service Worker是什麼超出了本文的內容範圍,但可以檢視Going Offline: Service Workers with Jeremy Keith部落格作為入門。

配置資料全部來自webpack.settings.js中的settings.workboxConfig物件。除了預先快取新版構建minifest.json中所有的資源外,我們還包括一個workbox-catch-handler.js來配置它以使用回退響應catch-all路由

// fallback URLsconst FALLBACK_HTML_URL = '/offline.html';
const FALLBACK_IMAGE_URL = '/offline.svg';
// This "catch" handler is triggered when any of the other routes fail to// generate a response.// https://developers.google.com/web/tools/workbox/guides/advanced-recipes#provide_a_fallback_response_to_a_routeworkbox.routing.setCatchHandler(({event, request, url
}) =>
{
// Use event, request, and url to figure out how to respond. // One approach would be to use request.destination, see // https://medium.com/dev-channel/service-worker-caching-strategies-based-on-request-types-57411dd7652c switch (request.destination) {
case 'document': return caches.match(FALLBACK_HTML_URL);
break;
case 'image': return caches.match(FALLBACK_IMAGE_URL);
break;
default: // If we don't have a fallback, just return an error response. return Response.error();

}
});
// Use a stale-while-revalidate strategy for all other requests.workbox.routing.setDefaultHandler( workbox.strategies.staleWhileRevalidate());
複製程式碼

MODULE.EXPORTS

最後,module.export使用webpack-mergewebpack.commons.js中的common.legacyConfig與我們的生產環境舊版配置合併,並將common.modernConfig與我們的生產環境新版配置合併:

// Production module exportsmodule.exports = [    merge(        common.legacyConfig,        { 
output: {
filename: path.join('./js', '[name]-legacy.[chunkhash].js'),
}, mode: 'production', devtool: 'source-map', optimization: configureOptimization(LEGACY_CONFIG), module: {
rules: [ configurePostcssLoader(LEGACY_CONFIG), configureImageLoader(LEGACY_CONFIG), ],
}, plugins: [ new CleanWebpackPlugin(settings.paths.dist.clean, configureCleanWebpack() ), new MiniCssExtractPlugin({
path: path.resolve(__dirname, settings.paths.dist.base), filename: path.join('./css', '[name].[chunkhash].css'),
}), new PurgecssPlugin( configurePurgeCss() ), new webpack.BannerPlugin( configureBanner() ), new HtmlWebpackPlugin( configureHtml() ), new WebappWebpackPlugin( configureWebapp() ), new CreateSymlinkPlugin( settings.createSymlinkConfig, true ), new SaveRemoteFilePlugin( settings.saveRemoteFileConfig ), new BundleAnalyzerPlugin( configureBundleAnalyzer(LEGACY_CONFIG), ), ].concat( configureCriticalCss() )
} ), merge( common.modernConfig, {
output: {
filename: path.join('./js', '[name].[chunkhash].js'),
}, mode: 'production', devtool: 'source-map', optimization: configureOptimization(MODERN_CONFIG), module: {
rules: [ configurePostcssLoader(MODERN_CONFIG), configureImageLoader(MODERN_CONFIG), ],
}, plugins: [ new webpack.optimize.ModuleConcatenationPlugin(), new webpack.BannerPlugin( configureBanner() ), new ImageminWebpWebpackPlugin(), new WorkboxPlugin.GenerateSW( configureWorkbox() ), new BundleAnalyzerPlugin( configureBundleAnalyzer(MODERN_CONFIG), ), ]
} ),];
複製程式碼

通過在我們的module.exports中返回一個陣列,我們告訴webpack有多個需要完成的編譯:一個用於舊版相容構建,另一個用於新版構建。

注意,對於舊版相容構建,我們將處理後的JavaScript輸出為[name]-legacy.[hash].js,而新版構建將其輸出為[name].[hash].js

通過將mode設定為production,我們告知webpack這是一個生產環境構建,這將啟用許多適用於生產環境的設定。

通過將devtool設定為source-map,我們要求將CSS/JavaScript生成單獨的.map檔案,這是我們更容易除錯實時生產環境網站,而無需新增資源的檔案大小。

這裡使用了幾個我們尚未涉及的webpack外掛:

  • CreateSymlinkPlugin —— 這是我建立的一個外掛,允許在構建過程中建立符號連結,使用它來將生成的favicon.ico符號連結到/favicon.ico,因為許多web瀏覽器在web根目錄中查詢。

  • SaveRemoteFilePlugin —— 用於下載遠端檔案並將其作為webpack構建過程的一部分輸出。我用它來下載和提供谷歌的分析。

  • ImageminWebpWebpackPlugin —— 此外掛會為專案匯入的所有JPEG和PNG檔案建立.webp變體。

直到現在,我們為專案提供了一個很好的生產環境構建。

TAILWIND CSS &
POSTCSS CONFIG

為了使webpack正確構建Tailwind CSS和其他css,我們需要做一些設定,感謝我的夥伴Jonathan Melville在構建這方面的工作,首先我們需要一個postcss.config.js檔案:

module.exports = { 
plugins: [ require('postcss-import'), require('postcss-extend'), require('postcss-simple-vars'), require('postcss-nested-ancestors'), require('postcss-nested'), require('postcss-hexrgba'), require('autoprefixer'), require('tailwindcss')('./tailwind.config.js') ]
};
複製程式碼

這可以儲存在專案根目錄中,PostCSS將在構建過程中自動查詢它,並應用我們指定的PostCSS外掛。請注意,這是我們引入tailwind.config.js檔案的位置,以便其成為構建過程的一部分。

最後,我們的CSS入口點app.pcss看起來像這樣:

/** * app.css * * The entry point for the css. * *//** * This injects Tailwind's base styles, which is a combination of * Normalize.css and some additional base styles. * * You can see the styles here: * https://github.com/tailwindcss/tailwindcss/blob/master/css/preflight.css */ @import "tailwindcss/preflight";
/** * This injects any component classes registered by plugins. * */@import '
tailwindcss/components';
/** * Here we add custom component classes;
stuff we want loaded * *before* the utilities so that the utilities can still * override them. * */@import '
./components/global.pcss';
@import '
./components/typography.pcss';
@import '
./components/webfonts.pcss';
/** * This injects all of Tailwind'
s utility classes, generated based on your * config file. * */@import 'tailwindcss/utilities';
/** * Include styles for individual pages * */@import './pages/homepage.pcss';
/** * Include vendor css. * */ @import 'vendor.pcss';
複製程式碼

顯然,對其進行定製以包括用於自定義css的任何元件/介面。

POST-BUILD PROJECT TREE

這是我們專案在構建後的結構:

├── example.env├── package.json├── postcss.config.js├── src│   ├── css│   │   ├── app.pcss│   │   ├── components│   │   │   ├── global.pcss│   │   │   ├── typography.pcss│   │   │   └── webfonts.pcss│   │   ├── pages│   │   │   └── homepage.pcss│   │   └── vendor.pcss│   ├── fonts│   ├── img│   │   └── favicon-src.png│   ├── js│   │   ├── app.js│   │   └── workbox-catch-handler.js│   └── vue│       └── Confetti.vue├── tailwind.config.js├── templates├── web│   ├── dist│   │   ├── criticalcss│   │   │   └── index_critical.min.css│   │   ├── css│   │   │   ├── styles.d833997e3e3f91af64e7.css│   │   │   └── styles.d833997e3e3f91af64e7.css.map│   │   ├── img│   │   │   └── favicons│   │   │       ├── android-chrome-144x144.png│   │   │       ├── android-chrome-192x192.png│   │   │       ├── android-chrome-256x256.png│   │   │       ├── android-chrome-36x36.png│   │   │       ├── android-chrome-384x384.png│   │   │       ├── android-chrome-48x48.png│   │   │       ├── android-chrome-512x512.png│   │   │       ├── android-chrome-72x72.png│   │   │       ├── android-chrome-96x96.png│   │   │       ├── apple-touch-icon-114x114.png│   │   │       ├── apple-touch-icon-120x120.png│   │   │       ├── apple-touch-icon-144x144.png│   │   │       ├── apple-touch-icon-152x152.png│   │   │       ├── apple-touch-icon-167x167.png│   │   │       ├── apple-touch-icon-180x180.png│   │   │       ├── apple-touch-icon-57x57.png│   │   │       ├── apple-touch-icon-60x60.png│   │   │       ├── apple-touch-icon-72x72.png│   │   │       ├── apple-touch-icon-76x76.png│   │   │       ├── apple-touch-icon.png│   │   │       ├── apple-touch-icon-precomposed.png│   │   │       ├── apple-touch-startup-image-1182x2208.png│   │   │       ├── apple-touch-startup-image-1242x2148.png│   │   │       ├── apple-touch-startup-image-1496x2048.png│   │   │       ├── apple-touch-startup-image-1536x2008.png│   │   │       ├── apple-touch-startup-image-320x460.png│   │   │       ├── apple-touch-startup-image-640x1096.png│   │   │       ├── apple-touch-startup-image-640x920.png│   │   │       ├── apple-touch-startup-image-748x1024.png│   │   │       ├── apple-touch-startup-image-750x1294.png│   │   │       ├── apple-touch-startup-image-768x1004.png│   │   │       ├── browserconfig.xml│   │   │       ├── coast-228x228.png│   │   │       ├── favicon-16x16.png│   │   │       ├── favicon-32x32.png│   │   │       ├── favicon.ico│   │   │       ├── firefox_app_128x128.png│   │   │       ├── firefox_app_512x512.png│   │   │       ├── firefox_app_60x60.png│   │   │       ├── manifest.json│   │   │       ├── manifest.webapp│   │   │       ├── mstile-144x144.png│   │   │       ├── mstile-150x150.png│   │   │       ├── mstile-310x150.png│   │   │       ├── mstile-310x310.png│   │   │       ├── mstile-70x70.png│   │   │       ├── yandex-browser-50x50.png│   │   │       └── yandex-browser-manifest.json│   │   ├── js│   │   │   ├── analytics.45eff9ff7d6c7c1e3c3d4184fdbbed90.js│   │   │   ├── app.30334b5124fa6e221464.js│   │   │   ├── app.30334b5124fa6e221464.js.map│   │   │   ├── app-legacy.560ef247e6649c0c24d0.js│   │   │   ├── app-legacy.560ef247e6649c0c24d0.js.map│   │   │   ├── confetti.1152197f8c58a1b40b34.js│   │   │   ├── confetti.1152197f8c58a1b40b34.js.map│   │   │   ├── confetti-legacy.8e9093b414ea8aed46e5.js│   │   │   ├── confetti-legacy.8e9093b414ea8aed46e5.js.map│   │   │   ├── precache-manifest.f774c437974257fc8026ca1bc693655c.js│   │   │   ├── styles-legacy.d833997e3e3f91af64e7.js│   │   │   ├── styles-legacy.d833997e3e3f91af64e7.js.map│   │   │   ├── vendors~confetti~vue.03b9213ce186db5518ea.js│   │   │   ├── vendors~confetti~vue.03b9213ce186db5518ea.js.map│   │   │   ├── vendors~confetti~vue-legacy.e31223849ab7fea17bb8.js│   │   │   ├── vendors~confetti~vue-legacy.e31223849ab7fea17bb8.js.map│   │   │   └── workbox-catch-handler.js│   │   ├── manifest.json│   │   ├── manifest-legacy.json│   │   ├── report-legacy.html│   │   ├── report-modern.html│   │   ├── webapp.html│   │   └── workbox-catch-handler.js│   ├── favicon.ico ->
dist/img/favicons/favicon.ico│ ├── index.php│ ├── offline.html│ ├── offline.svg│ └── sw.js├── webpack.common.js├── webpack.dev.js├── webpack.prod.js├── webpack.settings.js└── yarn.lock複製程式碼

INJECTING SCRIPT &
CSS TAGS IN YOUR HTML

通過這裡顯示的webpack配置,<
script>
<
style>
不會作為生產構建的一部分注入到HTML中,該設定使用Craft CMS,它具有模板系統,我們使用Twigpack外掛注入標籤。

如果你沒有使用Craft CMS或具有模板引擎的系統,並且希望將這些標記注入到HTML中,那麼需要使用HtmlWebpackPlugin執行此操作,這個配置已經包含在內,你只需要新增一個配置來告訴它將標籤注入到HTML。

CRAFT CMS 3 INTEGRATION WITH THE TWIGPACK PLUGIN

如果你沒有使用Craft CMS 3,可以跳過這一部分,它只是提供了一些有用的整合資訊。

用於前端開發的webpack4配置[帶註釋]

我寫了一個叫Twigpack的免費外掛,可以很容易地將我們的webpack構建設定與Craft CMS 3整合。

它處理manifest.json檔案以將入口點注入到Twig模板中,甚至用於處理執行舊版/新版模組注入,非同步css載入以及更多的模式。

它將使這裡介紹的webpack4配置非常簡單。

為了包含CSS,我這樣做:

<
!--# if expr="$HTTP_COOKIE=/critical\-css\=1/" -->
{{
craft.twigpack.includeCssModule("styles.css", false)
}
}<
!--# else -->
<
script>
Cookie.set("critical-css", '1', {
expires: "7D", secure: true
});
<
/script>
{{
craft.twigpack.includeCriticalCssTags()
}
} {{
craft.twigpack.includeCssModule("styles.css", true)
}
} {{
craft.twigpack.includeCssRelPreloadPolyfill()
}
}<
!--# endif -->
複製程式碼

<
!--#-->
HTML註釋是Nginx Servier Side Includes指令,模式是如果設定了critical-css cookie,使用者已經在過去7天訪問過我們的網站,那麼他們的瀏覽器應該有網站css快取,我們只是正常提供網站css。

如果沒有設定critical-css cookie,我們通過TinyCookie設定cookie,包括我們的Critical CSS,並非同步載入站點CSS。有關Critical CSS的詳細資訊,可以參考Implementing Critical CSS on your website文章。

為了提供我們的javascript,我們只需執行以下操作:

{{ 
craft.twigpack.includeSafariNomoduleFix()
}
}{{
craft.twigpack.includeJsModule("app.js", true)
}
}複製程式碼

第二個引數true告訴它將JavaScript非同步模組載入,因此生成的HTML如下所示:

<
script>
!function(){var e=document,t=e.createElement("script");
if(!("noModule"in t)&
&
"onbeforeload"in t){var n=!1;
e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;
else if(!e.target.hasAttribute("nomodule")||!n)return;
e.preventDefault()
},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()
}
}();
<
/script>
<
script type="module" src="http://example.test/dist/js/app.273e88e73566fecf20de.js">
<
/script>
<
script nomodule src="http://example.test/dist/js/app-legacy.95d36ead9190c0571578.js">
<
/script>
複製程式碼

有關詳細介紹,請檢視Twigpack文件。

這是我使用的完整config/twigpack.php檔案,請注意,它具有我在Homestead VM內部執行的本地設定,與你的設定可能不同:

return [    // Global settings    '*' =>
[ // If `devMode` is on, use webpack-dev-server to all for HMR (hot module reloading) 'useDevServer' =>
false, // The JavaScript entry from the manifest.json to inject on Twig error pages 'errorEntry' =>
'', // Manifest file names 'manifest' =>
[ 'legacy' =>
'manifest-legacy.json', 'modern' =>
'manifest.json', ], // Public server config 'server' =>
[ 'manifestPath' =>
'/dist/', 'publicPath' =>
'/', ], // webpack-dev-server config 'devServer' =>
[ 'manifestPath' =>
'http://localhost:8080/', 'publicPath' =>
'http://localhost:8080/', ], // Local files config 'localFiles' =>
[ 'basePath' =>
'@webroot/', 'criticalPrefix' =>
'dist/criticalcss/', 'criticalSuffix' =>
'_critical.min.css', ], ], // Live (production) environment 'live' =>
[ ], // Staging (pre-production) environment 'staging' =>
[ ], // Local (development) environment 'local' =>
[ // If `devMode` is on, use webpack-dev-server to all for HMR (hot module reloading) 'useDevServer' =>
true, // The JavaScript entry from the manifest.json to inject on Twig error pages 'errorEntry' =>
'app.js', // webpack-dev-server config 'devServer' =>
[ 'manifestPath' =>
'http://localhost:8080/', 'publicPath' =>
'http://192.168.10.10:8080/', ], ],];
複製程式碼

WRAPPING UP!

哇,這是一個深坑,當我第一次開始研究webpack時,我很快意識到它是一個非常強大的工具,具有非常強大的功能。你走多遠取決於你想要游到多深。

用於前端開發的webpack4配置[帶註釋]

有關本篇文章的完整原始碼,請檢視annotated-webpack-4-config倉庫。

希望這篇文章對你有所幫助,慢慢消化,將它做的更棒。

FURTHER READING

如果你想收到有關新文章的通知,請在推特上關注@nystudio107

來源:https://juejin.im/post/5be45723e51d45305c2ceaf0

相關文章