WEBPACK 配置的共同约定
我为所有 webpack 配置文件(webpack.common.js
,webpack.dev.js
和webpack.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
配置文件,它包含 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.js
导入为设置,以便我们可以访问那里的设置,并将 package.json
作为 pkg
导入,以便访问那里的一些设置。
CONFIGURATION 函数
看看 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) => {
});
这有两点需要说明:
- 通过
/* webpackChunkName:"vue" */
注释,我们告诉 webpack 我们想要这个动态代码拆分块被命名。 - 由于我们在
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 的更多信息,请查看 Working with Babel 7 and Webpack 这篇文章。
接下来我们来看 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.entries
从 webpack.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]'
}
}
]
};
};
字体加载在 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模块的入口点。不过对于构建的资源,两个清单中的键都是相同的。
接下来我们看一眼非常标准的 configureVueLoader()
函数:
// Configure Vue loader
const configureVueLoader = () => {
return {
test: /\.vue$/,
loader: 'vue-loader'
};
};
它可以让我们更轻松的加载 Vue 单文件组件。webpack 负责为您提取适当的 HTML,CSS 和 JavaScript。
BASE CONFIG (基础配置)
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(),
]
};
这里的所有内容都是按 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
中定义的文件。
MODERN 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.js
和 webpack.prod.js
使用。
// Common module exports
// noinspection WebpackConfigHighlighting
module.exports = {
'legacyConfig': merge(
legacyConfig,
baseConfig,
),
'modernConfig': merge(
modernConfig,
baseConfig,
),
};
下节讲 WEBPACK.DEV.JS 的解析: