Web 开发的带注释的 webpack 4 配置。
构建现代网站已成为定制应用程序开发。由于网站具有传统应用程序的功能,因此预计网站将不仅仅是营销网站。
每当流程变得复杂时,我们都会将其分解为可管理的组件,并使用工具自动化构建流程。无论我们是制造汽车、起草法律文件还是建立网站,都是如此。
为工作使用正确的工具 正是因为这个原因,像webpack这样的工具一直处于现代 Web 开发的最前沿:它们帮助我们构建复杂的东西。
webpack4对我来说最吸引我的是它在构建方面变得多么快。
webpack 让我更容易构建我现在正在制作的网站和应用程序类型,它还允许我使用最现代的工具链。
了解开发系统中的层如何工作是会带来好处的,最终,你只需要决定你想站在前端技术金字塔的哪个位置。
我希望 webpack 为我做的事情,以及我想在我的构建过程中加入的技术:
- [Development / Production] 在 本地开发中,我希望通过内存中的webpack,而对于生产构建(通常通过buddy.works),我希望进行所有可能的优化。因此,我们有单独
dev和prod构建。 - Hot Module Replacement 当我对我的 JavaScript、CSS 或模板进行更改时,我希望网页能够无缝刷新。这极大地加快了开发速度:只需对 Reload 按钮说不。
- Dynamic Code Splitting 我不想在配置文件中手动定义 JavaScript 块,我希望 webpack 为我整理出来。
- Lazy Loading 也就是异步动态模块加载。仅在需要时加载所需的代码/资源,而不会阻塞渲染。
- Modern & Legacy JS Bundles — 我想将现代 ES2015+ JavaScript 模块部署到支持它的全球 75% 以上的浏览器中
- Cache Busting via manifest.json- 这允许我们为静态资产设置较长的到期数据,同时确保它们在更改时自动缓存清除。
- Critical CSS 这可以使初始页面加载速度显着加快。
- Workbox Service Worker—— 我们可以利用 Google 的``Work kbox项目为我们生成一个 Service Worker,它将了解我们项目的所有资产。PWA,我们来了!
- PostCSS — 我认为它是 “CSS 的巴别塔”,诸如 SASS 和 SCSS 之类的东西都是建立在它之上的,它让你现在可以使用即将推出的 CSS 功能。
- Image Optimization
mozjpeg图像是迄今为止大多数网页上最大的东西,因此通过自动化工具(如 、 、等 )来优化它们是有意义的optipng``svgo - Automatic .webp Creation Chrome、Edge 和 Firefox 都支持 .webp,
.webp这是一种比 JPEG 更有效的格式。 - Tailwind CSS — Tailwind 是一种实用程序优先的 CSS,我用于在本地开发中快速制作原型,然后通过PurgeCSS 运行以进行
- Offline Compression of static resources ——我们可以将我们的静态资源预压缩成 .gz 文件,我们的网络服务器可以自动提供给接受它们的客户端。
还有更多,比如 JavaScript 的自动压缩、CSS优化以及我们期望前端构建系统提供的其他标准功能。
我还希望它与可能在本地开发环境中使用不同工具的开发团队合作,并让配置易于维护和在项目之间重用。
webpack 配置的通用约定
我为 webpack 配置文件采用了一些约定,webpack.common.js以webpack.prod.js使事情更加一致。
每个配置文件有两个内部配置:
- legacyConfig — 适用于旧版 ES5 构建的配置
- modernConfig 适用于现代 ES2015+ 构建的配置
我们这样做是因为我们有单独的配置来创建旧版本和现代版本。这使它们在逻辑上保持分离。webpack.common.js也有一个 baseConfig ; ****这纯粹是组织性的。
可以把它想象成面向对象编程,其中各种配置相互继承,baseConfig是根对象。
该webpack.dev.js配置没有传统和现代构建的概念;如果我们在本地开发中工作webpack-dev-server,我们可以假设一个现代版本。
我为保持配置干净和可读而采用的另一个约定是configure()为各种 webpack 插件和其他需要配置的 webpack 部分提供函数,而不是全部内联。
我这样做是因为一些来自webpack.settings.js需要的数据在被 webpack 使用之前需要进行转换,并且由于双重传统/现代构建,我们需要根据构建类型返回不同的配置。
它还使配置文件更具可读性。
作为一个通用的 webpack 概念,理解 webpack 本身只知道如何加载 JavaScript 和 JSON。要加载其他任何东西,我们需要使用loader。我们将在 webpack 配置中使用许多不同的加载器。
带注释的 webpack.common.js
现在让我们看一下我们的webpack.common.js配置文件,其中包含由dev和prod构建类型共享的所有设置。
// webpack.common.js - common webpack config
const LEGACY_CONFIG = 'legacy';
const MODERN_CONFIG = 'modern';
// node modules
const path = require('path');
const merge = require('webpack-merge');
// webpack plugins
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const WebpackNotifierPlugin = require('webpack-notifier');
// config files
const pkg = require('./package.json');
const settings = require('./webpack.settings.js');
在序言中,我们引入了我们需要的 Node 包,以及我们使用的 webpack 插件。然后我们导入我们的webpack.settings.jsassettings以便我们可以访问那里的设置,还导入我们的package.jsonaspkg以访问那里的一些设置。
还导入我们的package.jsonaspkg以访问那里的一些设置。
配置功能
// Configure Babel loader
const configureBabelLoader = (browserList) => {
return {
test: /.js$/,
exclude: settings.babelLoaderConfig.exclude,
cacheDirectory: true,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
sourceType: 'unambiguous',
presets: [
[
'@babel/preset-env', {
modules: false,
corejs: {
version: 2,
proposals: true
},
useBuiltIns: 'usage',
targets: {
browsers: browserList,
},
}
],
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-runtime',
],
},
},
};
};
该configureBabelLoader()函数将 配置babel-loader为处理所有以.js. 它使用@babel/preset-env而不是.babelrc文件,因此我们可以在 webpack 配置中将所有内容分开。
Babel 可以将现代 ES2015+ JavaScript(以及许多其他语言,如 TypeScript 或 CoffeeScript)编译为针对特定浏览器或标准集的 JavaScript。我们传入browserList作为参数,以便我们可以构建现代 ES2015+ 模块和旧版 ES5 JavaScript,并为旧版浏览器使用 polyfill。
通过设置useBuiltIns为,'usage'我们还告诉 babel 在每个文件的基础上应用单独的 pollyfills。这可以允许更小的包大小,因为它只包含我们使用的内容。有关这方面的更多信息,请查看使用 Babel 7 和 Webpack文章。
在我们的 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>
旧浏览器会忽略该type="module"脚本,并获取main-legacy.js. 现代浏览器加载main.js, 并忽略nomodule.
@babel/plugin-syntax-dynamic-import插件允许我们在Web 浏览器实现ECMAScript 动态导入提案之前进行动态导入。这让我们可以异步加载我们的 JavaScript 模块,并根据需要动态加载。
那么这是什么意思?这意味着我们可以这样做:
// App main
const main = async () => {
// Async load the vue module
const { default: Vue } = await import(/* webpackChunkName: "vue" */ 'vue');
// Create our vue instance
const vm = new Vue({
el: "#app",
components: {
'confetti': () => import(/* webpackChunkName: "confetti" */ '../vue/Confetti.vue'),
},
});
return vm;
};
// Execute async function
main().then( (vm) => {
});
// Accept HMR as per: https://webpack.js.org/api/hot-module-replacement#accept
if (module.hot) {
module.hot.accept();
}
这做了两个主要的事情:
- 通过
/* webpackChunkName: "vue" */注释,我们已经告诉 webpack 我们希望这个动态代码分割块被命名为什么 - 由于我们
import()在一个async函数(“main”)中使用,该函数await是我们动态加载的 JavaScript 导入的结果,而我们的其余代码继续其愉快的方式
我们已经有效地告诉 webpack 我们希望如何通过代码而不是通过配置来拆分我们的块。并且通过 的魔力@babel/plugin-syntax-dynamic-import,这个 JavaScript 块可以根据需要异步加载。
接下来我们有configureEntries():
// Configure Entries
const configureEntries = () => {
let entries = {};
for (const [key, value] of Object.entries(settings.entries)) {
entries[key] = path.resolve(__dirname, settings.paths.src.js + value);
}
return entries;
};
对于单页应用程序(SPA),您将只有一个入口点。对于更传统的网站,您可能有多个入口点(可能每页模板一个)。webpack.settings.js``settings.entries
无论哪种方式,因为我们已经在我们的 webpack.settings.js中定义了我们的入口点,所以在那里配置它们很容易。入口点实际上只是一个<script src="app.js"></script>标记,您将包含在 HTML 中以引导 JavaScript。
由于我们使用动态导入的模块,我们通常<script></script>在页面上只有一个标签;我们的 JavaScript 的其余部分会根据需要动态加载。
接下来我们有configureFontLoader()函数:
// Configure Font loader
const configureFontLoader = () => {
return {
test: /.(ttf|eot|woff2?)$/i,
use: [
{
loader: 'file-loader',
options: {
name: 'fonts/[name].[ext]'
}
}
]
};
};
字体加载对于构建dev和prod构建都是相同的,因此我们将其包含在此处。对于我们使用的任何本地字体,我们可以告诉 webpack 在我们的 JavaScript 中加载它们:
import comicsans from '../fonts/ComicSans.woff2';
接下来我们有configureManifest()函数:
// Configure Manifest
const 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"
}
我们传入一个文件名,因为我们创建了一个现代manifest.json和一个遗留manifest-legacy.json,它们分别具有我们现代 ES2015+ 模块和遗留 ES5 模块的入口点。对于为现代和旧版本构建的资源,两个清单中的键是相同的。
baseConfig 和 modernConfig与 legacyConfig文件合并:
// The base webpack config
const 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(),
]
};
模块.出口
最后,module.exports使用webpack-merge包将配置合并在一起,并返回一个由webpack.dev.js和webpack.prod.js使用的对象。
// Common module exports
// noinspection WebpackConfigHighlighting
module.exports = {
'legacyConfig': merge.strategy({
module: 'prepend',
plugins: 'prepend',
})(
baseConfig,
legacyConfig,
),
'modernConfig': merge.strategy({
module: 'prepend',
plugins: 'prepend',
})(
baseConfig,
modernConfig,
),
};
带注释的 webpack.dev.js
现在让我们看看我们的webpack.dev.js配置文件,其中包含在我们处理项目时用于开发构建的所有设置。它与设置合并webpack.common.js以形成完整的 webpack 配置。
// webpack.dev.js - developmental builds
// node modules
const merge = require('webpack-merge');
const path = require('path');
const webpack = require('webpack');
// webpack plugins
const DashboardPlugin = require('webpack-dashboard/plugin');
// config files
const common = require('./webpack.common.js');
const pkg = require('./package.json');
const settings = require('./webpack.settings.js');
在webpack.dev.js配置中,没有现代和传统构建的概念,因为在本地开发中,当我们使用时webpack-dev-server,我们可以假设一个现代构建。
在序言中,我们再次引入了我们需要的 Node 包和我们使用的 webpack 插件。然后我们导入我们的webpack.settings.js作为settings以便我们可以访问那里的设置,还导入我们的package.json作为pkg以访问那里的一些设置。
我们还导入了webpack.common.js我们将合并开发设置的通用 webpack 配置。
配置功能
configureDevServer():
// Configure the webpack-dev-server
const configureDevServer = () => {
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()),
disableHostCheck: true,
hot: true,
overlay: true,
watchContentBase: true,
watchOptions: {
poll: !!parseInt(settings.devServerConfig.poll()),
ignored: /node_modules/,
},
headers: {
'Access-Control-Allow-Origin': '*'
},
};
};
当我们进行生产构建时,webpack 会捆绑我们所有的各种资产并将它们保存到文件系统中。相比之下,当我们在本地开发中处理项目时,我们通过webpack-dev- server使用开发构建:
- 启动为我们的资产提供服务的本地Express Web 服务器
- 为了速度,在内存中而不是文件系统中构建我们的资产
- 当我们更改它们并通过热模块替换 (HMR)将它们注入网页时,将重建 JavaScript、CSS组件等资产,而无需重新加载页面
- 当我们对模板进行更改时将重新加载页面
因此,不是在我的 webpack.settings.js 文件中硬编码本地开发环境(因为它可能因团队中的每个人而异),webpack.settings.js 可以从您自己的可选 .env 文件中读取特定的 devServer 配置:
# webpack example settings for Homestead/Vagrant
PUBLIC_PATH="/dist/"
DEVSERVER_PUBLIC="http://192.168.10.10:8080"
DEVSERVER_HOST="0.0.0.0"
DEVSERVER_POLL=1
DEVSERVER_PORT=8080
DEVSERVER_HTTPS=0
我们还使用PUBLIC_PATH.env 变量(如果存在)来允许生产构建的每个环境构建。这样我们就可以进行本地生产构建,或者我们可以在 Docker 容器中进行分发生产构建,该容器使用准备好通过 CDN 分发的 URL 进行构建。
configureImageLoader():
// Configure Image loader
const configureImageLoader = () => {
return {
test: /.(png|jpe?g|gif|svg|webp)$/i,
use: [
{
loader: 'file-loader',
options: {
name: 'img/[name].[hash].[ext]'
}
}
]
};
};
需要注意的是,这仅适用于我们的 webpack 构建中包含的图像;许多其他图像将来自其他地方(CMS 系统、资产管理系统等)。
为了让 webpack 知道一张图片,你将它导入到你的 JavaScript 中:
import Icon from './icon.png';
接下来是我们的configurePostcssLoader():\
// Configure the Postcss loader
const configurePostcssLoader = () => {
return {
test: /.(pcss|css)$/,
use: [
{
loader: 'style-loader',
},
{
loader: 'vue-style-loader',
},
{
loader: 'css-loader',
options: {
url: false,
importLoaders: 2,
sourceMap: true
}
},
{
loader: 'resolve-url-loader'
},
{
loader: 'postcss-loader',
options: {
sourceMap: true
}
}
]
};
};
我们使用PostCSS 来处理我们所有的 CSS,包括Tailwind CSS 。我认为它是 CSS 的通天塔,因为它将各种高级 CSS 功能编译为浏览器可以理解的普通旧 CSS。
需要注意的是,对于 webpack 加载器,它们的处理顺序与它们列出的相反:
- postcss-loader — 以 PostCSS 加载和处理文件
- resolve-url-loader — 将 CSS 中的任何 s 重写
url()为公共路径相关 - css-loader — 解析我们所有的 CSS
@import和url()s - style-loader — 将我们所有的 CSS 注入到文档中的内联
<style></style>标签
请记住,因为这是我们在本地开发中所做的,所以我们不需要做任何花哨的事情来将我们所有的 CSS 提取到一个最小化的文件中。取而代之的是,我们只是将style-loader其全部内联到我们的文档中。
它将为我们的webpack-dev-serverCSS 使用热模块替换 (HMR),因此每当我们更改任何内容时,它都会重新构建我们的 CSS 并自动重新注入它。这有点神奇。
我们通过包含它来告诉 webpack 我们的 CSS:
import styles from '../css/app.pcss';
这在 webpack 文档的加载 CSS部分中有详细讨论。
我们从App.js入口点开始这样做;将此视为 PostCSS 入口点。该app.pcss文件@import是我们项目使用的所有 CSS;稍后将对此进行详细介绍。
模块.出口
最后,module.exports使用webpack-merge包common.modernConfig与我们的开发配置合并:
// Development module exports
module.exports = merge(
common.modernConfig,
{
output: {
filename: path.join('./js', '[name].[hash].js'),
publicPath: settings.devServerConfig.public() + '/',
},
mode: 'development',
devtool: 'inline-source-map',
devServer: configureDevServer(),
module: {
rules: [
configurePostcssLoader(),
configureImageLoader(),
],
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new DashboardPlugin(),
],
}
);
通过设置mode,'development'我们告诉 webpack 这是一个开发版本。
通过设置devtool为'inline-source-map'将我们的 CSS/JavaScript内联到文件本身中。这使得文件很大,但便于调试。
webpack.HotModuleReplacementPlugin在webpack方面支持热模块替换(HMR)。
DashboardPlugin 插件让我们感觉像一个拥有精美 webpack 构建 HUD的宇航员。
带注释的 webpack.prod.js
现在让我们看看我们的webpack.prod.js配置文件,其中包含在我们处理项目时用于生产构建的所有设置。它与设置合并webpack.common.js以形成完整的 webpack 配置。
// webpack.prod.js - production builds
const LEGACY_CONFIG = 'legacy';
const MODERN_CONFIG = 'modern';
// node modules
const 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 plugins
const 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 files
const common = require('./webpack.common.js');
const pkg = require('./package.json');
const settings = require('./webpack.settings.js');
此类是Tailwind CSS的自定义PurgeCSS提取器,允许在类名中使用特殊字符。
// Custom PurgeCSS extractor for Tailwind that allows special characters in
// class names.
//
// https://github.com/FullHuman/purgecss#extractor
class TailwindExtractor {
static extract(content) {
return content.match(/[A-Za-z0-9-_:/]+/g) || [];
}
}
这取自Tailwind CSS 文档的使用 PurgeCSS 删除未使用的 CSS部分。有关此提取器如何与 PurgeCSS 一起使用以神奇地使您的 CSS 苗条和整洁的详细信息,请参见下文。
配置功能
configureBanner():
// Configure file banner
const 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 信息的横幅。
接下来是configureBundleAnalyzer():
// Configure Bundle Analyzer
const 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 生成的捆绑包中的确切内容。
它非常有用,可以帮助我减小包大小,并准确了解 webpack 正在构建什么,因此我将它作为我生产构建过程的一部分。
接下来是configureCriticalCss():\
// Configure Critical CSS
const 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通过从我们的.settings.criticalCssConfig.pages``webpack.settings.js
请注意,如果传入的页面settings.criticalCssConfig.ampPrefix名称中有任何地方,它会通过传入非常大的高度为整个网页(不仅仅是折叠内容上方)生成 CriticalCSS。
接下来是configureCleanWebpack():\
// Configure Clean webpack
const configureCleanWebpack = () => {
return {
cleanOnceBeforeBuildPatterns: settings.paths.dist.clean,
verbose: true,
dry: false
};
};
这只是使用CleanWebpackPlugin
接下来是configureCompression():\
// Configure Compression webpack plugin
const configureCompression = () => {
return {
filename: '[path].gz[query]',
test: /.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false,
compressionOptions: {
numiterations: 15,
level: 9
},
algorithm(input, compressionOptions, callback) {
return zopfli.gzip(input, compressionOptions, callback);
}
};
};
这使用CompressionPlugin 将我们的静态资源预压缩到文件中,这样我们就可以通过简单的webserver配置.gz将它们提供预压缩。
接下来是configureHtml():
// Configure Html webpack
const configureHtml = () => {
return {
templateContent: '',
filename: 'webapp.html',
inject: false,
};
};
这使用HtmlWebpackPlugin和WebappWebpackPlugin(见下文)为我们的网站图标生成HTML。请注意,我们传入一个空字符串,templateContent以便输出只是 WebappWebpackPlugin 的原始输出。
接下来是configureImageLoader():
// Configure Image loader
const 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';
接下来是我们的configureOptimization():
// Configure optimization
const 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 将项目范围内使用的所有到单个文件中。如果您以前使用过 webpack,那么您过去可能使用过 ExtractTextPlugin 来执行此操作。
然后,我们还使用OptimizeCSSAssetsPlugin通过cssnano删除重复规则和最小化CSS来优化生成的 CSS。。
最后,我们将 JavaScript 最小化器设置为TerserPlugin ;这是因为UglifyJsPlugin 不再支持最小化ES2015+ JavaScript。由于我们正在生成现代 ES2015+ 包,我们需要它。
接下来是configurePostcssLoader():\
// Configure Postcss loader
const 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 的通天塔,因为它将各种高级 CSS 功能编译为浏览器可以理解的普通旧 CSS。
同样,重要的是要注意,对于 webpack 加载器,它们的处理顺序与它们列出的相反:
- postcss-loader — 以 PostCSS 加载和处理文件
- resolve-url-loader — 将 CSS 中的任何 s 重写
url()为公共路径相关 - css-loader — 解析我们所有的 CSS
@import和url()s - MiniCssExtractPlugin.loader — 将我们所有的生产 CSS 提取到一个文件中
由于这是一个生产版本,我们提取了所有随处使用的 CSS MiniCssExtractPlugin.loader,并将其保存到一个.css文件中。CSS 也被最小化,并针对生产进行了优化。
我们通过包含它来告诉 webpack 我们的 CSS:\
import styles from '../css/app.pcss';
这在 webpack 文档的加载 CSS部分中有详细讨论。
我们从App.js入口点开始这样做;将此视为 PostCSS 入口点。该app.pcss文件@import是我们项目使用的所有 CSS;稍后将对此进行详细介绍。
接下来是configurePurgeCss():\
// Configure PurgeCSS
const 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/whatever 文件,并删除任何未使用的 CSS。
节省的费用可能是巨大的;Tailwind CSS 和 PurgeCSS 是天作之合。
它遍历所有路径 glob 以settings.purgeCssConfig.paths查找要保留的 CSS 规则;任何未找到的 CSS 规则都会从我们生成的 CSS 构建中删除。
当我们知道我们不想删除某些 CSS 时,我们还使用WhitelisterPlugin 可以轻松地将整个文件甚至 glob 列入白名单。与我们匹配的所有文件中的 CSS 规则都settings.purgeCssConfig.whitelist被列入白名单,并且永远不会从生成的构建中删除。
接下来是configureTerser():
// Configure terser
const configureTerser = () => {
return {
cache: true,
parallel: true,
sourceMap: true
};
};
这只是配置了TerserPlugin 使用的一些设置最小化我们的遗留和现代 JavaScript 代码。
接下来是configureWebApp():
// Configure Webapp webpack
const 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,
}
};
};
这使用WebappWebpackPlugin以多种格式生成我们所有的网站图标,以及我们的 webapp 和其他manifest.jsonPWA细节。
它与HtmlWebpackPlugin 一起工作,还可以输出一个文件,webapp.html其中包含指向所有生成的 favicon 和相关文件的链接,以包含在我们的 HTML 页面的<head></head>.
接下来是configureWorkbox():
// Configure Workbox service worker
const configureWorkbox = () => {
let config = settings.workboxConfig;
return config;
};
我们使用 Google 的WorkboxWebpackPlugin 为我们的网站生成一个 Service Worker 。解释什么是 Service Worker 超出了本文的范围,请查阅别的资料了解入门。
配置全部来自settings.workboxConfig我们的webpack.settings.js. 除了在我们的现代构建中预缓存所有资产外manifest.json,我们还包括一个workbox-catch-handler.js将其配置为使用回退响应包罗万象的路线。
// fallback URLs
const 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_route
workbox.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使用webpack-merge将common.legacyConfigfromwebpack.common.js与我们的生产遗留配置以及common.modernConfig与我们的生产现代配置合并:
// Production module exports
module.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 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 CleanWebpackPlugin(
configureCleanWebpack()
),
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因为许多网络浏览器在网络根目录中寻找。 - SaveRemoteFilePlugin 插件用于下载远程文件并将它们作为 webpack 构建过程的一部分发出。我用它在本地下载和提供 Google 的
analytics.js - ImageminWebpWebpackPlugin 插件
.webp项目导入的所有 JPEG 和 PNG 文件的
就是这样,我们现在为我们的项目提供了一个很好的生产构建,其中包含所有的花里胡哨。
Tailwind CSS 和 PostCSS 配置
为了让 webpack 正确构建 Tailwind CSS 和我们的其余 CSS,我们需要做一些设置。首先我们需要一个postcss.config.js文件:
module.exports = {
plugins: [
require('postcss-import')({
plugins: [
require('stylelint')
]
}),
require('tailwindcss')('./tailwind.config.js'),
require('postcss-preset-env')({
autoprefixer: { grid: true },
features: {
'nesting-rules': true
}
})
]
};
这可以存储在项目根目录中;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 的任何组件/页面。
构建后项目树
这是我们的项目树在构建后的样子:
├── 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
在 HTML 中注入脚本和 CSS 标签
使用此处显示的 webpack 配置,<script>标签<style>不会作为生产构建的一部分注入到您的 HTML 中。该设置使用具有模板系统的Craft CMS,我们使用Twigpack 插件注入标签。
如果您不使用 Craft CMS 或具有模板引擎的系统,并且希望将这些标签注入到您的 HTML 中,您将需要使用HtmlWebpackPlugin为您完成此操作。该插件已经包含在内,您只需要添加一些配置来告诉它将标签注入您的 HTML。
希望这对您有所帮助,