实战 webpack 4 配置解析二

本文深入探讨Webpack4配置,介绍其通用约定、模块加载原理及动态导入策略,演示如何通过配置实现ES5与ES6+代码的并行构建。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

接上篇:实战 webpack 4 配置解析一

WEBPACK 配置的共同约定

我为所有 webpack 配置文件(webpack.common.jswebpack.dev.jswebpack.prod.js)采用了一些约定,以便一致性。

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

  • legacyConfig - 适用于旧版 ES5 构建的配置
  • modernConfig - 适用于新版 ES6+ 构建的配置

我们这样做的原因是我们有单独的配置来进行旧版和新版的构建。这使它们在逻辑上分开。webpack.common.js 也有一个 baseConfig; 这纯粹是形式上的。

可以把它想象成面向对象编程,其中各种配置相互继承,baseConfig 是根对象。

我为保持配置清晰和可读而采用的另一个约定是为各种 webpack 插件和需要配置的其他 webpack 片段配置 configure() 函数,而不是全部内联。

我这样做是因为来自 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.js 导入为设置,以便我们可以访问那里的设置,并将 package.json 作为 pkg 导入,以便访问那里的一些设置。

CON­FIG­U­RA­TION 函数

看看 configureBabelLoader() 函数长什么样子:

// Configure Babel loader
const 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+ JavaScript(以及许多其他语言,如 TypeScript 或CoffeeScript)编译为针对特定浏览器或标准下的 JavaScript。我们将 browserList 作为参数传递,这样我们不仅可以构建旧版浏览器现代 ES2015+ 模块还能用 polyfill 构建旧版的 ES5 JavaScript。

在我们的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浏览器实现ECMAScript 动态导入提案之前就进行动态导入。这使我们可以异步加载我们的JavaScript 模块,并根据需要动态加载。

那么这意味着什么?它意味着我们可以这样干:

// App main
const 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 function
main().then( (value) => {
});

这有两点需要说明:

  1. 通过 /* webpackChunkName:"vue" */ 注释,我们告诉 webpack 我们想要这个动态代码拆分块被命名。
  2. 由于我们在 async 函数(“main”)中使用 import(),该函数 await 我们动态加载的 JavaScript 导入的结果,而其余的代码继续欢快的执行。

我们已经有效地告诉 webpack 我们希望怎样对代码进行块分割,而不是通过配置。并且通过 @babel/plugin-syntax-dynamic-import 的神奇功能,可以根据需要异步加载此 JavaScript 块。

请注意,我们也对 .vue 单个文件组件做了同样的事情。nice。

为了替代 await,我们也可以在 import() Promise 返回后执行我们的代码:

// Async load the vue module
import(/* 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'),
        },
    });
});

在这里,我们使用了 import() Promise 来代替 await ,因此我们可以在明确动态导入后愉快地使用 Vue。

如果您仔细看,您可以看到我们通过 Promises 有效地解决了JavaScript 依赖关系。太棒了!

我们甚至可以在用户点击某些内容,滚动到某个位置或满足其他条件后加载某些JavaScript 块等做一些有趣的事情。查看 模块方法 import() 了解更多信息。

如果您有兴趣了解有关 Babel 的更多信息,请查看 Work­ing with Babel 7 and Web­pack 这篇文章。

接下来我们来看 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;
};

在这里,我们通过 settings.entrieswebpack.settings.js 中拿到 webpack 入口点。对于单页面应用(SPA),您只有一个入口点。对于更传统的网站,您可能有几个入口点(每页模板可能有一个入口点)。

无论哪种方式,因为我们已经在 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;
        },
    };
};

这里描述了一个基于文件名缓存破解的 web­pack-man­i­fest-plu­g­in 插件 。简而言之,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模块的入口点。不过对于构建的资源,两个清单中的键都是相同的。

接下来我们看一眼非常标准的 configureVueLoader() 函数:

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

它可以让我们更轻松的加载 Vue 单文件组件。webpack 负责为您提取适当的 HTML,CSS 和 JavaScript。

BASE CON­FIG (基础配置)

baseConfig 会与 modernConfiglegacyConfig 进行合并。

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

这里的所有内容都是按 webpack 的标准进行,但请注意我们将 vue$ 作为 vue/dist/vue.esm.js 的别名,以便我们可以获得 Vue 的 ES2015 模块版本。

我们使用 WebpackNotifierPlugin 以友好的方式告诉我们构建的状态。

LEFACY CONFIG (旧版配置)

legacyConfig 用于使用适当的 polyfill 构建 ES5 旧版的 JavaScript :

// Legacy webpack config
const 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 中定义的文件。

MOD­ERN CONFIG(新版配置)

modernConfig 用于构建现代 ES2015 JavaScript 模块,没有多余的东西:

// Modern webpack config
const 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 WebpackConfigHighlighting
module.exports = {
    'legacyConfig': merge(
        legacyConfig,
        baseConfig,
    ),
    'modernConfig': merge(
        modernConfig,
        baseConfig,
    ),
};

下节讲 WEBPACK.DEV.JS 的解析:

实战 webpack 4 配置解析三

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值