对于webpack的部分总结

727 阅读14分钟

Web 开发的带注释的 webpack 4 配置。

构建现代网站已成为定制应用程序开发。由于网站具有传统应用程序的功能,因此预计网站将不仅仅是营销网站。

每当流程变得复杂时,我们都会将其分解为可管理的组件,并使用工具自动化构建流程。无论我们是制造汽车、起草法律文件还是建立网站,都是如此。

为工作使用正确的工具 正是因为这个原因,像webpack这样的工具一直处于现代 Web 开发的最前沿:它们帮助我们构建复杂的东西。

webpack4对我来说最吸引我的是它在构建方面变得多么快。

webpack 让我更容易构建我现在正在制作的网站和应用程序类型,它还允许我使用最现代的工具链。

了解开发系统中的层如何工作是会带来好处的,最终,你只需要决定你想站在前端技术金字塔的哪个位置。

我希望 webpack 为我做的事情,以及我想在我的构建过程中加入的技术:

还有更多,比如 JavaScript 的自动压缩、CSS优化以及我们期望前端构建系统提供的其他标准功能。

我还希望它与可能在本地开发环境中使用不同工具的开发团队合作,并让配置易于维护和在项目之间重用。

企业微信截图_ecd63b3a-f030-4c5a-bcd5-6bc3ecaf6667.png

webpack 配置的通用约定

我为 webpack 配置文件采用了一些约定,webpack.common.jswebpack.prod.js使事情更加一致。

每个配置文件有两个内部配置:

  • legacyConfig —   适用于旧版 ES5 构建的配置
  • mod­ern­Con­fig   适用于现代 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配置文件,其中包含由devprod构建类型共享的所有设置。

// 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();
}

这做了两个主要的事情:

  1. 通过/* webpackChunkName: "vue" */注释,我们已经告诉 webpack 我们希望这个动态代码分割块被命名为什么
  2. 由于我们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]'
                }
            }
        ]
    };
};

字体加载对于构建devprod构建都是相同的,因此我们将其包含在此处。对于我们使用的任何本地字体,我们可以告诉 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.jswebpack.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@importurl()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-mergecommon.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 生成的捆绑包中的确切内容。

企业微信截图_31690688-2bfe-4255-8000-bbadbb9db6a8.png 它非常有用,可以帮助我减小包大小,并准确了解 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,
    };
};

这使用HtmlWebpackPluginWebappWebpackPlugin(见下文)为我们的网站图标生成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 加载器,它们的处理顺序与它们列出的相反:

由于这是一个生产版本,我们提取了所有随处使用的 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-mergecommon.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。

希望这对您有所帮助,