JavaScript生态系统中,webpack作为捆绑我们代码的主要工具脱颖而出。但是,在我们深入研究webpack如何做到这一点(这是我们帖子的要点)之前,让我们首先了解什么是ES模块。
什么是ES模块?
ES模块(ESM)是为Node.js和浏览器编写代码的推荐方式。目前,所有主要浏览器都支持ES模块,它们代表了在网络上打包和重用JavaScript代码的官方标准格式。
关于一些前置知识,包括如何使用ES模块,并简要介绍了Node.js生态系统中ESM的演变。这篇文章将主要侧重于扩展第一个帖子,其中我们澄清了Node.js模块生态系统多年来是如何演变的——从CommonJS模块系统到当前、完全准备好的、生产级别的ESM支持,这是今天建议使用的符合规范、向后兼容和可互操作的模块系统。
正如我们在之前的帖子中提到的那样,我们注意到我们可以在模块系统中定义和使用带有export和import关键字的软件包。此外,我们指出了源代码或库作者可以指示Node.js运行时将JavaScript代码视为ESM的不同方式,因为Node运行时默认将JavaScript代码视为CommonJS模块。
在这篇文章中,我们将扩展我们之前的帖子,并优化使用ECMAScript模块规范编写的代码,使其与旧的Node.js版本具有可移植性和互操作性。
导入和导出功能
请参阅以下如何导入和导出具有ES模块语法的函数的示例:
// smallestNumber.js
export function smallestNumber(arr) {
let smallestNumber = arr[0]
let smallestIndex = 0
for(let i = 1; i < arr.length; i++) {
if(arr[i] < smallestNumber) {
smallestNumber = arr[i]
smallestIndex = i
}
}
return smallestNumber;
}
请注意,正如我们可能知道的那样,export关键字很有用,因为它允许我们向需要调用它的其他模块提供smallestNumber函数。除了正则函数外,我们还可以导出常量、类,甚至简单的变量。另请注意,我们可以使用ESM进行默认导出。
// result.js从'./smallestNumber.js'导入{最小数字};console.log(最小数字([3,2,5,6,0,-1]))//返回-1
请注意,对于import关键字,我们可以引用我们从ESM模块或包中导出的smallestNumber函数,smallestNumber.js。
回顾一下,关于这些主题的更多详细信息——包括ES模块的概要以及如何使用它们——可以在其他文章中找到。ESM的文档广泛涵盖了这个主题,包括我们之前的博客文章中没有涉及的其他更广泛的主题。
传输代码以向后兼容
在下一节中,我们最终将专注于代码转汇编过程的机制,但在此之前,让我们首先了解这个过程的重要性。
正如我们之前提到的,旧的JavaScript版本,如ES5,需要能够运行我们的新代码,并理解其语法。这意味着需要有一种方法使语言完全向后兼容。
向后兼容性是向编程语言添加新功能时需要考虑和优先考虑的最重要方面之一。对于ESM,当它设置为位于项目根目录的package.json文件中的module时,Node.js运行时能够根据type字段确定它应该默认的模块系统/格式。
//package.json文件{ "类型": "模块"}
上述设置使所有与package.json文件处于文件夹结构相同级别的文件默认为ESM。或者,我们可以决定将类型设置为commonjs,节点运行时将强制所有文件符合默认的Common JS模块系统。虽然如果指定了notype字段,这通常是默认行为。
在我们之前的博客文章中,我们还介绍了其他方法,例如,使用.mjs或.cjs文件格式,以及如何将这些扩展解析为ESM或其他模块格式。
本质上,当模块被标记为ECMAScript模块时,节点运行时使用不同的模式或方法来解决文件导入。例如,导入现在更严格,这意味着我们必须为相对路径或请求附加完整的文件名及其扩展名。
什么是代码转编?
随着新的JavaScript版本的引入和语法(也称为ES2015)的更改,TypeScript、JavaScript超集和其他语言的引入,例如CoffeeScript——编写无处不在的JavaScript不再像以前那样简单。
正如我们可能已经知道的,不同的浏览器有不同的JavaScript引擎,它们对这些新JS功能的不同采用和支持水平可能不是一致的,因为它们在相同的时间没有满足语言规范。这导致了代码可以在一个浏览器上工作而不能在另一个浏览器上工作的情况。
因此,转编的本质是能够将新的JS ES2015语法转换为旧的ES5语法,以便代码可以在旧的浏览器上运行。
例如,模板文字或,例如,null coalescing和其他ES2015或ES2015+功能,仍然没有完整的浏览器支持和服务器端运行时支持,因此我们可能需要传输代码来支持这些版本。
主要是指Babel、Traceur等称为加载器的工具与webpack一起用于传输代码。
在高层次上,转写员通过逐行读取源代码并产生等效的输出来跨编程语言工作。例如,我们可能想要将TypeScript代码库传输到普通的旧JavaScript。
一般来说,转子允许我们放心地使用新的、非标准化的JavaScript功能。目前,在Node.js和浏览器环境中使用ES模块的最佳方法是使用Babel将它们传输到CommonJS模块格式。
在Node.js中传输
默认情况下,ESM带有一个转发器加载器,该加载器使用加载器钩将节点运行时无法理解的源代码转换为普通JS。
在这里,我们可以根据需要从磁盘加载源代码,但在Node.js为我们执行之前。这通常不适用于浏览器环境,因为通过电线单独获取每个文件会非常缓慢且不高性能。
转载器加载器还附带一个resolve钩子,告诉运行时如何处理未知文件类型。
什么是webpack?
Webpack是一个构建工具,有助于将我们的代码及其依赖项捆绑到单个JavaScript文件中。我们还可以说,webpack是JavaScript应用程序的静态模块捆绑器。这是因为它将摇树和编译(包括编译和最小化步骤)等技术应用于我们的源代码。
像webpack这样的捆绑器与转运器携手合作。这意味着它们是完全不同的,而是互补的工具集。因此,我们需要配置webpack以与转发器一起工作——比如Babel。
正如我们之前提到的,转译器要么执行将一种语言编译为另一种语言的工作,要么使一种语言向后兼容。Webpack与Babel配合得很好,也很容易配置。例如,我们可以通过使用Babel插件创建webpack配置文件(webpack.config.js)来配置Babel与webpack一起工作——事实上,webpack插件生态系统是webpack的组成。
另一方面,Babel可以使用ababel.config.js文件或.babelrc文件进行配置。
为什么是webpack?
您可能知道,webpack支持几种开箱即用的模块类型,包括CommonJS和ES模块。Webpack也适用于客户端和服务器端JavaScript,因此使用webpack,我们还可以轻松处理图像、字体、样式表等资产和资源。
它仍然是一个非常强大的工具,因为它根据文件导入和导出自动构建和推断依赖图(因为在引擎盖下,每个文件都是一个模块)。将此与加载器和插件相结合,使webpack成为我们武器库中的绝佳工具。有关如何在引擎盖下工作的更多详细信息,请参阅文档。
在插件方面,webpack还拥有丰富的插件生态系统。插件支持webpack做一些肮脏的工作,如优化捆绑包、管理资产等。
总之,与webpack等工具捆绑是当今工作或确保与模块向后兼容的最快方式,因为ESM正在逐渐成为生态系统中代码重用的官方标准的势头。
Webpack还支持加载程序,这有助于它决定如何处理、捆绑和处理非原生模块或文件。重要的是要指出webpack如何处理加载程序。装载机从下到上进行评估和执行。因此,最后一个加载器首先以此类推,以此类推,按顺序执行。
在这篇文章中,我们的主要重点是webpack如何传输或处理ECMAScript模块。
使用装载机进行转运
加载器将文件从一种编程语言转换为另一种编程语言。例如,ts-loader可以将TypeScript转换或转换为JavaScript。通常,我们使用加载程序作为开发依赖项。例如,让我们看看如何使用ts-loader。
要安装,我们可以运行以下内容:
npm install --save-dev ts-loader
然后,我们可以使用此加载器来指示webpack正确处理源代码中的所有TypeScript文件。请参阅下面的samplewebpack.config.js文件。
module.exports = {模块:{规则:[ { test: /.ts$/, use: 'ts-loader' }, ], },};
在这里,正如我们所提到的,我们告诉webpack处理所有以.ts扩展名结尾的文件路径,并将其编译为浏览器和节点运行时可以理解的JavaScript语法。这意味着加载器也可以在Node.js环境中运行,因此也遵循模块分辨率标准。
加载器命名约定
命名加载器的一般方式是一致的,因为加载器的名称和连字符命名——通常为xxxloader。例如,babel-loader、ts-loader等。
对于我们的用例,我们对ESNext或babel-loader特别感兴趣,这是一个目前在7.16版本中构建和支持的社区加载器。有关加载器的更多信息可以在webpack文档中找到。
一些webpack核心概念
为了了解webpack的工作原理,本节涵盖了读者应该注意的一些高级概念。
正如我们前面提到的,webpack使用依赖关系图,这意味着它递归地构建一个包含应用程序需要或依赖的每个模块的关系,然后将所有这些模块捆绑到可供使用的输出文件中。这意味着webpack需要有一个入口点——事实上确实如此。
在设置我们的webpack配置时,入口点是文件路径webpack检查的开始,然后开始为我们的应用程序构建内部依赖关系图。请注意,webpack还支持我们应用程序的多个入口点。
module.exports = {条目:['./path/to/my/entry1/file1.js', './path/to/my/entry2/file2.js']};
要添加多个入口点,我们可以改用一个数组。在webpack v5中,我们现在可以有一个空的条目对象。这允许我们在添加条目时使用插件。
在webpack内部构建依赖关系图并完成捆绑过程后,它需要将捆绑包输出到另一个文件路径,然后我们需要提供该文件路径。
这就是output属性的作用。它告诉webpack发出它创建的捆绑包的路径,以及文件如何命名。
const path = require('path');module.exports = {条目:'./path/to/my/entry/file.js',输出:{路径:path.resolve(__dirname, 'dist'),文件名:'my-first-webpack.bundle.js', },};
从v4.0.0开始,webpack不需要配置文件来捆绑您的项目,尽管它确实假设您已将应用程序的入口点与文件夹和文件路径定义为src/index.js,并且输出被捆绑到thedist/main.js文件夹路径中,最小化,优化并准备生产。
设置webpack和Babel
完全支持使用原生ES2015模块语法。这意味着我们可以使用import和export语句,而无需依赖外部编译工具或Babel等依赖项。然而,仍然建议配置Babel,以防webpack尚未考虑其他较新的ES2015+功能。
根据到位的模块格式,webpack检查最近的package.json文件并执行适当的建议。当使用webpack捆绑我们的代码时,通常建议坚持使用单个模块语法,以允许webpack以一致的方式正确处理捆绑的输出,从而防止不必要的错误。
To get started, we need to ensure we have the webpack CLI installed on our machines. We can then make use of the init CLI command to quickly spin up a webpack config based on our project requirements. We can do so by simply running npx webpack-cli init and responding to the prompts appropriately.
现在,我们需要将我们的ES2015代码编译为ES5,以便我们可以在不同的浏览器环境或运行时中使用它。为此,我们需要安装Babel及其所有webpack所需的依赖项。
让我们继续安装以下内容:
Babel Loader,一个与Babel Core接口的webpack加载器
要安装,我们可以运行:
npm i webpack webpack-cli webpack-dev-server @babel/core @babel/preset-env babel-loader rimraf -D
在安装结束时,我们的package.json应该看起来像这样:
{ "名称": "webpack-demo", "版本": "1.0.0", “描述”:“带有babel的webpack演示”, "main": "dist/bundle.js", "脚本":{ "build": "node_modules/.bin/webpack --config webpack.config.js --mode=production", "watch": "node_modules/.bin/webpack --config webpack.config.js --mode=development -w", "prebuild:dev": "rimraf dist" }, "作者": "Alexander Nnakwue", "许可证": "MIT", 依赖性:{}, "devDependencies": { "@babel/core": "^7.16.0", "@babel/preset-env": "^7.16.4", "babel-loader": "^8.2.3", "rimraf": "^3.0.2", "webpack": "^5.64.3", "webpack-cli": "^4.9.1", "webpack-dev-server": "^4.5.0" }, "类型": "模块"}
请注意,我们固定了依赖项,以便在将来运行应用程序时保持一致性。
@babel/preset-env包将把所有ES2015-ES2020代码转换为ES5或基本上我们在目标选项中指定的任何目标环境。当我们打算设置特定目标时,我们通常需要它,然后允许Babel瞄准该特定环境。但它也可以在不设置目标的情况下使用,但成本更大。
此软件包基本上根据自己的内部映射检查指定的目标环境,然后编译它传递给Babel的插件列表以进行代码编译。这导致JavaScript捆绑包变小。
因此,如果我们未能指定目标,输出的代码大小将更大,因为默认情况下,Babel插件将ECMAScript语法功能分组到相关功能的集合中。有关此的更多详细信息可以在文档中找到。
更好的是,有了更小的捆绑大小和更大的性能提升的选项,我们可以利用babel/preset-modules,这些模块最终将被合并到@babel/preset-env核心中。
配置webpack.config.js文件
现在,让我们继续配置我们的webpack文件。继续在我们的项目根目录中创建一个新的webpack.config.js文件。
从“路径”导入路径;从“url”导入{fileURLToPath};const __dirname = path.dirname(fileURLToPath(import.meta.url));导出默认{条目:'./src/index.js',输出:{路径:path.resolve(__dirname, './dist'),文件名:'bundle.js', },实验:{outputModule:true, },插件:[ //空插件数组 ],模块:{ // https://webpack.js.org/loaders/babel-loader/#root规则:[ {测试:/.m?js$/,装载机:“babel-loader”,排除:/node_modules/, } ], },devtool:“源地图”}
在上面的配置中,我们为__dirname变量创建了一个polyfill。此外,我们已将字段outputModule设置为true,如果我们打算使用webpack编译供他人使用的公共库,这是必要的。babel-loader加载ES2015+代码,并使用Babel将其传输到ES5。
正如您在配置文件中看到的,我们有一个module属性,它有一个rule属性,其中包含一个数组,用于配置我们webpack配置可能需要的单个加载器。
在这里,我们添加了webpack加载器,并根据我们的项目要求设置了所需的选项。
请注意,test选项是与每个文件的绝对路径匹配的正则表达式,并检查文件扩展名。在上面的示例中,我们正在测试我们的文件是以a.mjs或.js扩展名结尾。
使用(不使用).babelrc文件
现在,我们可以通过创建.babelrc文件来配置Babel,也可以在项目的根目录中创建。文件内容如下所示:
{ "预设":[ 【 "@babel/preset-env", { 目标:{ esmodules:正确 } } 】 】 }
以下是使用npm run watch命令在本地开发中运行我们的应用程序的输出:
如果我们不想使用.babelrc文件,我们还可以在rules数组中的options对象中添加预设,例如:
模块:{规则:[ {测试:/.m?js$/,排除:/node_modules/,使用:{装载机:“babel-loader”,选项:{预设:['@babel/preset-env', "es2015", "es2016"], } } } 】}
设置预设和添加插件
我们需要设置预设,以便将代码中的ES2015功能转换为ES5。在options数组中,我们还可以将我们想要添加到配置中的插件。
例如,我们可以添加此行:plugins:['@babel/plugin-transform-runtime'],它安装一个Babel运行时,该运行时禁用每个文件的自动运行时注入以防止膨胀。
要将此包含在我们的代码中,我们需要通过运行来安装它们:
npm安装-D @babel/plugin-transform-runtime
我们还必须通过运行npm install @babel/runtime将@babel/runtime添加为依赖项。
另请注意,在rules对象中,我们告诉webpack排除带有exclude属性的node_modules文件夹中的文件。这是为了让我们有一个更快的捆绑过程,因为我们不想捆绑我们的node_modules文件夹。
在某些情况下,我们希望webpack处理ES 2015模块。为了允许这一点,我们需要使用一个名为theModuleConcatenationPlugin的插件。该插件在webpack中启用了某种形式的串联行为,称为范围提升,这是ESM语法使该功能成为可能。默认情况下,此插件已在生产模式下启用,否则将禁用。
结论
为了避免兼容性问题,我们需要将我们的代码从ES2015传输到ES5。这就是webpack发挥作用的地方。Webpack将我们的代码捆绑在一起,并将编译后的版本输出到配置文件中指定的目标。
在我们的webpack配置文件中,模块规则允许我们指定不同的加载器,这是显示加载器的简单方法。在这篇文章中,我们只使用了巴别塔装载机,但我们也可以在生态系统中使用许多其他装载机。
在最新webpack版本v5的改进和更改方面,现在对异步模块提供了开箱即用的支持。顾名思义,异步模块是基于Promise的,因此不会同步解析。现在,通过require()导入异步模块也将返回一个Promise,该Promise将解析为导出。
此外,在Node.js生态系统中,package.json文件中现在支持导出和导入字段。最后,在最新版本中,最低支持的Node.js版本已从v6.0提升到v10.13.0,这同样是LTS版本。