来自 Web前端 2020-03-24 06:26 的文章
当前位置: 网上澳门金莎娱乐 > Web前端 > 正文

网上澳门金莎娱乐前端模块化

时间: 2019-11-04阅读: 91标签: 模块化背景

时间: 2019-05-29阅读: 311标签: 模块几乎每种编程语言都能将组成程序的代码拆分为多个文件。 在 C 和 C++ 中#include指令就用于这个目的,而 Java 和 Python 有import关键字。 JavaScript 是迄今为止为数不多的例外之一,但新的 JavaScript 标准(ECMAScript 6)通过引入所谓的 ECMAScript 模块来改变这一点。所有主流浏览器都支持这个新标准 —— 只有 Node.js 似乎落后了。这是为什么?

众所周知,早期 JavaScript 原生并不支持模块化,直到 2015 年,TC39 发布 ES6,其中有一个规范就是ES modules(为了方便表述,后面统一简称 ESM)。但是在 ES6 规范提出前,就已经存在了一些模块化方案,比如 CommonJS(in Node.js)、AMD。ESM 与这些规范的共同点就是都支持导入(import)和导出(export)语法,只是其行为的关键词也一些差异。

新的 ECMAScript(ES)模块与以前的语言版本不完全兼容,因此使用的 JavaScript 引擎需要知道每一个文件是“旧” JavaScript 代码还是“新”模块。

CommonJS

例如在 ECMAScript 5 中引入的许多程序员首选的严格模式曾经是可选的,必须明确启用才行,同时它在 ES 模块中始终处于活动状态。因此,以下代码段在语法上可以解释为传统的 JavaScript 代码和 ES 模块:

// add.jsconst add = (a, b) = a + bmodule.exports = add// index.jsconst add = require('./add')add(1, 5)
a =5;

AMD

作为经典的 Node.js 模块,这相当于global.a = 5,因为未声明变量a并且未明确激活严格模式,因此a被视为全局变量。如果你尝试加载与 ES 模块相同的文件,则会收到错误 “ReferenceError:a is not defined”,因为未声明的变量可能无法在严格模式下使用。

// add.jsdefine(function() { const add = (a, b) = a + b return add})// index.jsrequire(['./add'], function (add) { add(1, 5)})

浏览器通过script网上澳门金莎娱乐,标记的扩展解决了区别问题:没有type属性或带有type="text/javascript"属性的脚本仍然在传统模式下运行,而当脚本使用type ="module"属性时则作为模块处理。由于这种简单的分离,现在所有流行的浏览器都支持新的模块。 Node.js 中的实现要困难得多:2009年发明的 JavaScript 应用程序框架使用 CommonJS 标准模块,该标准基于require函数。此函数可以随时根据其相对于当前运行模块的路径加载另一个模块。新的 ES 模块也是由它们的路径定义的,但是 Node.js 是如何知道正在加载的模块是遗留的 CommonJS 还是 ES 模块的呢?仅仅基于语法是不够的,因为即使不使用新关键字的 ES 模块也不兼容CommonJS模块。

ESM

此外,ECMAScript 6 还提供了可以从 URL 加载模块,而 CommonJS 仅限于文件的相对和绝对路径。这种创新不仅使加载更复杂,而且可能更慢,因为 URL 不需要指向本地文件。特别是在浏览器中,脚本和模块通常通过HTTP网络协议加载。

// add.jsconst add = (a, b) = a + bexport default add//index.jsimport add from './add'add(1, 5)

CommonJS 允许通过require函数加载模块,该函数返回加载的模块。例如,CommonJS 模块可能如下所示:

ESM 的出现不同于其他的规范,因为这是 JavaScript 官方推出的模块化方案,相比于 CommonJS 和 AMD 方案,ESM采用了完全静态化的方式进行模块的加载。

const { readFile } = require('fs');const myModule = require('./my-module');

ESM规范模块导出

这不是 ECMAScript 6 中的一个选项,因为在require()调用期间,模块在 HTTP 上加载时可能会长时间阻止整个程序的执行。相反,ES 模块提供了两种加载其他模块的方法。在大多数情况下,使用import是有意义的:

模块导出只有一个关键词:export,最简单的方法就是在声明的变量前面直接加上 export 关键词。

import { readFile } from 'fs';import myModule from './my-module';
exportconstname ='Shenfq'

但是,这会不可避免地延迟模块的执行,直到加载fs./my-module,但它们不会阻止其他模块的执行。当模块必须动态加载时,会变得更加复杂。 CommonJS 模块中看起来微不足道的东西变得越来越难以异步:

可以在 const、let、var 前直接加上 export,也可以在 function 或者 class 前面直接加上 export。

if (condition) { myOtherModule = require('./my-other-module');}
export function getName() { return name}export class Logger { log(...args) { console.log(...args) }}

ECMAScript 希望通过功能性使用import关键字来解决这个问题,该关键字异步加载模块并在每次调用时返回Promise对象。但缺点是程序员现在也负责错误处理,因为错误不会像在同步情况下那样自动传给调用者。

上面的导出方法也可以使用大括号的方式进行简写。

if (condition) { import('./my-other-module.js') .then(myOtherModule = { // Module was loaded successfully and can // now be used here. }) .catch(err = { // An error occurred that needs to be handled here. console.error(err); });}
const name = 'Shenfq'function getName() { return name}class Logger { log(...args) { console.log(...args) }}export { name, getName, Logger }

如果使用async关键字声明了要加载模块的函数,由于 ECMAScript 6 中引入了await函数,import()的使用更加清晰,并且错误处理被传递给同步执行中的调用者:

最后一种语法,也是我们经常使用的,导出默认模块。

if (condition) { myOtherModule = await import('./my-other-module');}
const name = 'Shenfq'export default name

import作为一个函数使用,它不是 ECMAScript 6 的一个组件,而是一个所谓的 Stage 3 提案,有可能会在下一个 JavaScript 版本中标准化。此外 Firefox、Chrome 和 Safari 等许多浏览器以及 Node.js 都支持它。

模块导入

在Node.js中使用

模块的导入使用import,并配合from关键词。

区分 CommonJS 和 ES 模块的难度导致在 Node.js 下为 ES 模块引入了新的文件扩展名:如果已设置了-experimental-modules选项, Node.js 可以把以.mjs结尾的文件作为 ES 模块进行加载。从 2017 年 9 月发布的 Node.js 8.5.0 开始,如果将以下代码保存为testmodule.mjs,则可以用node -experimental-modules testmodule.mjs命令执行它:

// main.jsimport name from './module.js'// module.jsconst name = 'Shenfq'export default name
export function helloWorld(name) { console.log(`Hallo, ${name}!`);} helloWorld('javascript-conference.com');

这样直接导入的方式,module.js中必须使用export default,也就是说 import 语法,默认导入的是default模块。如果想要导入其他模块,就必须使用对象展开的语法。

Node.js 12 扩展了对 ES 模块的支持。重要的是,现在可以用package.json文件,它包含了诸如包的唯一名称之类的信息。现在使用的 JSON 格式扩展了一个名为type的新属性。可以选择将其更改为commonjsmodule以确定默认情况下应加载的包中所包含的 JavaScript 文件的模式。以下配置指定了一个包example-package,它至少必须包含 ES 模块index.js

// main.jsimport { name, getName } from './module.js'// module.jsexport const name = 'Shenfq'export const getName = () = name
{ "name": "example-package", "type": "module", "main": "index.js"}

如果模块文件同时导出了默认模块,和其他模块,在导入时,也可以同时将两者导入。

像往常一样,“main”字段指定哪个文件应该作为入口点。例如index.js模块可能如下所示:

// main.jsimport name, { getName } from './module.js'//module.jsconst name = 'Shenfq'export const getName = () = nameexport default name
import { userInfo } from 'os'; export function greet() { return `Hello ${userInfo().username}!`;}

当然,ESM 也提供了重命名的语法,将导入的模块进行重新命名。

现在可以从其他文件加载此模块。包通常位于node_modules目录中各自的文件夹中。要加载刚创建的包,我们可以用以下目录结构和一个名为main.js的新文件:

// main.jsimport * as mod from './module.js'let name = ''name = mod.namename = mod.getName()// module.jsexport const name = 'Shenfq'export const getName = () = name
- main.js+ node_modules + example-package - package.json - index.js

上述写法就相当于于将模块导出的对象进行重新赋值:

main.js文件可以引用传统的 CommonJS 或新的 ECMAScript 模块。在这两种情况下,example-package都不能使用通常的require()调用加载,因为 ECMAScript 模块必须始终异步加载。因此 CommonJS 模块必须使用import加载 ES 模块:

// main.jsimport { name, getName } from './module.js'const mod = { name, getName }
import('example-package').then(package = { console.log(package.greet());}).catch(err = { console.error(err);});

同时也可以对单独的变量进行重命名:

这样做的缺点是 CommonJS 模块不能像往常那样在开始时访问其他模块或软件包,但只能在事实和异步之后才能访问。执行如上所述脚本:node -experimental-modules main.js,如果入口点本身也是 ES 模块,则更容易。如果将main.js重命名为main.mjs,则可以用import

// main.jsimport { name, getName as getModName }
import { greet } from 'example-package';console.log(greet());

导入同时进行导出

因此,可以在一个应用程序中同时使用 CommonJS 和 ECMAScript 模块,但它有可能会引发混乱。因为 CommonJS 模块需要知道正在加载的模块是 CommonJS 还是 ES 模块,并且只能异步加载 ES 模块。这也适用于通过 npm 安装的软件包的加载。fscrypto等内置模块可以通过两种方式加载。

如果有两个模块 a 和 b ,同时引入了模块 c,但是这两个模块还需要导入模块 d,如果模块 a、b 在导入 c 之后,再导入 d 也是可以的,但是有些繁琐,我们可以直接在模块 c 里面导入模块 d,再把模块 d 暴露出去。

Node.js 中的差异

// module_c.jsimport { name, getName } from './module_d.js'export { name, getName }

除了异步加载依赖项的问题之外,Node.js 中的旧模块和新模块之间还存在进一步的差异。特别是 ES 模块中不再提供 Node.js 的特定功能,如变量__dirname,__filename,exportmodule__dirname__filename可以根据需要从新的import.meta对象重建:

这么写看起来还是有些麻烦,这里 ESM 提供了一种将 import 和 export 进行结合的语法。

import { fileURLToPath } from 'url';import { dirname} from 'path'; const __filename = fileURLToPath(import.meta.url);const __dirname = dirname(__filename);
export{ name, getName }from'./module_d.js'

变量moduleexports已被删除而无需替换;这同样适用于module.filenamemodule.idmodule.parent等属性。同样require()require.main不再可用。

上面是 ESM 规范的一些基本语法

虽然 CommonJS 中的循环依赖关系已经通过缓存各个模块的module.exports对象来解决,但 ECMAScript 6 用了所谓的绑定。简而言之,ES 模块不会导出和导入值,只是对值的引用。导入此类引用的模块可以访问该值,但无法修改它。已导出引用的模块可以为引用分配新值,该值将由从该点导入引用的其他模块使用。与之前的概念相比,这有着本质的区别,后者允许在任何时间点将属性分配给 CommonJS 模块的module.exports对象,从而使这些更改仅部分反映在其他模块中。

ESM 与 CommonJS 的差异

根据 ECMAScript 规范,import默认情况下不会用文件扩展名完成文件路径,因为 Node.js 之前已经为 CommonJS 模块完成了,因此必须明确说明。同样当指定的路径是目录时,行为会发生变化:import'./directory'不会在指定的文件夹中查找index.js文件,而是抛出一个错误,这是 Node.js 中的标准情况。两者都可以通过传递实验选项-es-module-specifier-resolution = node来改变。

首先肯定是语法上的差异,前面也已经简单介绍过了,一个使用import/export语法,一个使用require/module语法。

结论

另一个 ESM 与 CommonJS 显著的差异在于,ESM 导入模块的变量都是强绑定,导出模块的变量一旦发生变化,对应导入模块的变量也会跟随变化,而 CommonJS 中导入的模块都是值传递与引用传递,类似于函数传参(基本类型进行值传递,相当于拷贝变量,非基础类型【对象、数组】,进行引用传递)。

在最近发布的 Node.js 12.1.0 中,仍然需要通过-experimental-modules选项显式激活 ECMAScript 模块的使用,因为它是一个实验性功能。但是,开发人员的目标是在 Node.js 12 成为新的长期支持版本之前,在没有明确激活的情况下完成此功能并支持 ES 模块,预计将会在2019年10月完成。

下面我们看下详细的案例:

现有的各种 CommonJS 模块使从 CommonJS 到 ECMAScript 模块的转换变得复杂。单个程序包无法切换到 ES 模块,从而不会发生与使用require()加载相应程序包的现有程序和程序包不兼容的情况。像 Babel 这样的工具可以将较新的语法转换为与旧环境兼容的代码,这使转换更容易。像Deno这样的新框架背弃了近年来多样化的模块化系统,完全依赖于 ECMAScript 模块,这对于把 JavaScript 作为编程语言的开发,标准化模块的引入是重要的一步,为未来的改进铺平了道路。

CommonJS

作者:Tobias Nießen,翻译:疯狂的技术宅原文:-modules-node-js-status-quo-159508.html

// a.jsconst mod = require('./b')setTimeout(() = { console.log(mod)}, 1000)// b.jslet mod = 'first value'setTimeout(() = { mod = 'second value'}, 500)module.exports = mod

$ node a.jsfirst value

ESM

// a.mjsimport { mod } from './b.mjs'setTimeout(() = { console.log(mod)}, 1000)// b.mjsexport let mod = 'first value'setTimeout(() = { mod = 'second value'}, 500)

$ node --experimental-modules a.mjs# (node:99615) ExperimentalWarning: The ESM module loader is experimental.second value

另外,CommonJS 的模块实现,实际是给每个模块文件做了一层函数包裹,从而使得每个模块获取require/module、__filename/__dirname变量。那上面的a.js来举例,实际执行过程中a.js运行代码如下:

// a.js(function(exports, require, module, __filename, __dirname) { const mod = require('./b') setTimeout(() = { console.log(mod) }, 1000)});

而 ESM 的模块是通过import/export关键词来实现,没有对应的函数包裹,所以在 ESM 模块中,需要使用import.meta变量来获取__filename/__dirname。import.meta是 ECMAScript 实现的一个包含模块元数据的特定对象,主要用于存放模块的url,而 node 中只支持加载本地模块,所以 url 都是使用file:协议。

import url from 'url'import path from 'path'// import.meta: { url: file:///Users/dev/mjs/a.mjs }const __filename = url.fileURLToPath(import.meta.url)const __dirname = path.dirname(__filename)

加载的原理

步骤:

本文由网上澳门金莎娱乐发布于Web前端,转载请注明出处:网上澳门金莎娱乐前端模块化

关键词: