Node Packages

之前一直被nodejs的模块执行规则所困扰,这种困扰来自于目前Commonjs和ESModule在Node端能够并存的情况,什么时候能够执行ESModule,什么时候不能执行;在引用第三方包node_modules,具体的包是根据什么来区分是ESModule还是Commonjs。 通过阅读了Nodejs的原文档,这些问题也随之迎刃而解,所以特此在本篇做一做记录。 注:以下内容都是基于Nodejs V20.5.0 进行编写。 Nodejs中的模块系统 在Node环境下,如今共存着两种模块规范:ESModule和Commonjs,前者是ES6中提出模块化规范,所以为了统一整个Javascript的模块化规范,Nodejs也逐渐的由之前主流的Commonjs规范转变为ESModule,只不过为了兼容Nodejs老版本的代码,所以现在这两种模块在Node中是并存的关系,但是前者(ESModule)是趋势。 关于Commonjs和ESMoudle的模块技术细节,例如语法区别,加载规则等在本篇不会提到,接下来将会它们在nodejs中的设置以及引用规则。 模块的执行与加载 Node在执行代码前会先去判断代码是ES模块还是commonjs模块,Node会从三个角度依次判断代码的模块归属。 ES模块 对于ES模块,当出现以下情况时,Nodejs会将文件视为ES模块。 扩展名为.mjs的文件。 项目所归属的package.json的type字段的值为module,此时文件扩展名为.js的文件都视为ES模块。 字符串作为参数传入 --eval,或通过 STDIN 管道传输到 node,带有标志 --input-type=module。 .mjs扩展名的文件被视为ES模块的优先级是最高的,无论它身处何方,只要Node发现其是.mjs的文件,就会将其视作ES模块来执行。 commonjs模块 对于commonjs模块,当出现以下情况时,Nodejs会将文件视为commonjs模块。 扩展名为.cjs的文件。 项目所属的package.json的type字段的值为commonjs时,此时文件扩展名为.js的文件都视为commonjs模块。 字符串作为参数传入 --eval 或 --print,或通过 STDIN 管道传输到 node,带有标志 --input-type=commonjs。 和ES模块类似,如果文件扩展名为.cjs,那么Nodejs都会将其视作commonjs模块。 如果不设置type字段,那么该项目将会默认为commonjs语法的项目。 模块加载器 Nodejs有两种加载器用于分别解析说明符和加载ES模块和commonjs模块。 commonjs模块加载器: 🌟 处理require()调用,并且是同步加载模块的, 🌟 支持以文件夹作为模块。 🌟 不能用于加载`ECMAScript模块(ESModule)。 🌟 解析路径时,如果未找到完全匹配项,那么将尝试添加扩展名**.js,.json,.node,然后最后再尝试解析文件夹作为模块**。 将.json文件视为JSON文本文件,可以直接使用require加载。 特别注意上述的四个带🌟的规则,在平时日常开发中应该是能够经常遇见的。从第4点也能知道为什么使用require引用包时,可以不带文件的扩展名,因为node在解析路径时会自动添加。 ECMAScript模块加载器: 🌟 处理import()和import表达式,并且是异步加载模块的。 🌟 只接收Javascript文本文件的.js,.mjs和.cjs扩展名。 🌟 可以加载JSON模块,但需要导入断言。 🌟 不支持文件夹作为模块,必须完全指定目录索引。(例如./dir/index.js)。 🌟 可以加载Javascript Commonjs模块,导入的cjs模块会通过cjs-module-lexer来尝试识别命名的导出,同时导入的CommonJS模块会将其URL转化为绝对路径,然后通过CommonJS模块加载器加载。 需要注意的是第五点:ESModule可以加载CommonJS模块的文件,这和Commonjs语法是不同的。 以.mjs结尾的文件总是加载为ESModule,不会管最近的package.json。 以.cjs结尾的文件总是加载为commonjs,不会管最近的package.json。 包入口点 package.json中定义了该node项目的包信息,那么如果该package想被另一个package所引用,正如我们开发时引用第三方包那样import xxx from "xxx",那么需要在package中定义包的入口点,及入口文件。 main字段 main字段可以定义包的入口点,它适用于ES模块和CommonJS模块入口点,并且支持所有Nodejs版本。 但是它的缺点也显而易见,只能定义一个入口点,及包的主要入口点。 例如我们package的文件结构如下所示。 ├── package....

September 9, 2023 · 2 min · 249 words · Runtus

迭代器and生成器

很多的数据结构都具备迭代的性质,但是不同的数据结构的迭代方法有所不同,往往需要知道具体的数据结构来选择对应的迭代方法,例如数组的迭代可以通过索引来进行迭代。 为了统一迭代接口,使得我们可以不了解具体的数据结构的前提下也能进行迭代,所以有了迭代器这么一个概念,而生成器则是基于迭代器的一种能够控制函数流程的方法,生成器基于迭代器的原理运行,反过来生成器也能够快速构建迭代器。 迭代器 当需要对某个迭代对象进行迭代处理时,由于迭代之前需要事先知道如何使用数据结构,以及遍历顺序并不是数据结构固有的,所以想寻求某种机制去统一迭代过程,对每一种可迭代类型,都用同一种迭代方法,从而增加开发体验。(即无需事先知道如何迭代去实现迭代操作) 于是基于以上原因,诞生了迭代器概念,意在统一化所有迭代对象的处理方式。 可迭代协议 一个对象是可迭代对象,则需要暴露一个属性作为默认迭代器,并且该属性的key为Symbol.iterator,它的value是一个工厂函数,用于返回一个新迭代器。 js提供了一系列可以对可迭代对象进行操作的原生结构,如下图所示。 for-of 数组解构 扩展操作符号(即...) Array.from 创建集合 创建映射 Promise.all()接受由Promise组成的可迭代对象。 Promise.rice()接受由期约组成的可迭代对象。 yield*操作符,在生成器中使用。 上述谈到的原生结构在对可迭代对象进行操作时,会默认调用工厂函数生成一个迭代器,然后对迭代器进行操作。 迭代器协议 1. next和IteratorResult 可迭代协议描述了一个对象具有可迭代性质的要求和前提,而迭代器协议则是用于规范迭代器具有的性质和方法。 迭代器对象具有一个方法next,通过迭代器APInext()能够在可迭代对象中遍历数据,每次调用next都能获取到一个IteratorResult对象,其中包含迭代器返回的下一个值,如下所示。 const array = [1, 2, 4]; // 迭代器对象 const iter = array[Symbol.iterator](); console.log(iter.next()) console.log(iter.next()) console.log(iter.next()) console.log(iter.next()) console.log(iter.next()) // output // { value: 1, done: false } // { value: 2, done: false } // { value: 4, done: false } // { value: undefined, done: true } // { value: undefined, done: true } 如上的输出结果所示,IteratorResult包含两个属性value和done,value表示本次迭代获取的值,而done则表示迭代是否结束,这从另一个角度也说明迭代器只能通过next方法来获取迭代器的当前位置。 2....

August 23, 2023 · 3 min · 543 words · Runtus

Web components

Web-Components Web-Components是一项标准,规范,目前它包含了三项主要技术: Custom Elements自定义元素:(标签)它是一组JavaScript API,能够自定义Element以及其行为。 Shadow DOM影子DOM。 HTML templatesHTML模板:通过<template>和<slot>元素编写不在呈现页面中显示的标记模板。 通过这三个特性的共同作用能够创建封装功能的定制元素,在说明Web-Component的用法之前,先简单说明上述三项特性。 Custom Elements 自定义元素是Web Components中的一个重要特性,它能够让开发者将HTML页面(或者页面中的某个功能)封装为custom elements,从而达到复用的目的。目前支持custom elements的浏览器有FireFox,Chrome,Opera。 Custom Elements的管理是通过CustomElementRegistry接口进行操作的,其用于处理Web文档中的custom elements,同时它还提供注册自定义元素和查询已注册元素的方法,它的实例通过window.customElements属性来获得。 CustomElementRegistry接口有四个方法: CustomElementRegistry.define():定义一个新的自定义元素。 CustomElementRegistry.get():返回指定自定义元素的构造函数,如果未自定义元素,则返回undefined。 CustomElementRegistry.upgrade(): 更新一个自定义元素。 CustomElementRegistry.whenDefined():执行并返回一个已经定义的自定义元素的promise,即如果定义了这么一个元素,那么返回对应的promise。 CustomElementRegistry.define() 该方法是四个方法中最重要的方法,用于创建自定义元素,它接受三个参数: 自定义元素的名称,且其必须符合元素名称的DOMString标准字符串。 用于定义元素行为的类。 一个包含 extends 属性的配置对象,是可选参数。它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。 // 自定义Div class SelfDiv extends HTMLElement { constructor(){ // super方法的调用是必须得 super() // 元素的相关业务代码 } } // 使用customElements实例(CustomElementRegistry接口)来完成注册功能 customElements.define("word-count", SelfDiv, { extends: "div" }); 上述代码便是自定义元素注册的简单演示。 shadow DOM shadown DOM也叫影子DOM,它最主要的功能是做封装,将元素的标记结构,样式以及行为隐藏起来,与外界隔离,这样能够保证封装的代码既不会被外界影响,同时也能保证内部的代码不会影响到外部的元素,这样便实现了Web Component的解耦合。 Shadow DOM接口可以将一个隐藏的,独立的DOM附加到一个元素上,目前为止FireFox,Chrome,Opera,Safari默认支持Shadow DOM,Chromium内核的Edge也支持。 Shadow DOM允许将隐藏的DOM树附加到常规的DOM树中 => 以shadow root节点为起始根节点(Shadow Root的创建后续会说明),在这根节点的下方可以添加任何DOM元素,和普通的DOM元素没有任何区别。 下面有个示意图可以帮助理解。 上图的一些概念下面做一些解释: Shadow host:常规的DOM节点,Shadow DOM将会挂载到此处。 Shadow Tree:Shadow DOM内部的DOM树。 Shadow root: Shadow tree 的根节点。 注:Shadow DOM的操作方式和普通DOM操作方式没有任何区别,包括添加元素,设置属性等等,只不过Shadow DOM内部的任何改变都影响不了外部的DOM元素。 Shadow DOM的创建和挂载通过方法ElementShadow()来实现。 Element....

August 3, 2023 · 2 min · 339 words · Runtus

Rust Trait

Rust中的特征Trait类似于其他语言中的接口,它定义了一组可以被共享的行为,只要实现了特征,就能使用这组行为。 特征 Trait 特征的定义 通过trait关键字对特征进行定义。 pub trait Student { fn GoClass(&self); fn LeaveClass(&self); fn getClassRoom(&self) -> String; } 上述声明了**身为学生应该有的几个特征行为:**即上课,下课和获取教室房间号,那么其他具有学生性质的结构体(或者说为类)需要遵循该特征。 需要注意:特征只是坐函数签名,并不是真正的实现函数,函数的实现在绑定了该特征的类里实现,下面一节会提到。 类型实现特征 使用for关键字来为类实现特征。 pub struct Bob { pub year: String, pub sex: String } impl Student for Bob { // 实现特征 fn GoClass(&self){ // .... } fn LeaveClass(&self) { // ... } fn getClassRoom(&self) { // ... } } 在Bob类型中实现了Student特征声明的三个函数,这是必须的,除非在特征中有默认的实现。 如果特征中有默认的函数实现,那么绑定的类型可以不用再次实现函数,若实现了对应的函数,那么会覆盖默认的特征函数实现。如下所示。 pub trait Student { fn GoClass(&self){ println!("this is student!"); } fn LeaveClass(&self); fn getClassRoom(&self) -> String; } impl Student for Bob { fn GoClass(&self){ println!...

July 17, 2023 · 2 min · 282 words · Runtus

Rust包管理

本文参考了Rust语言圣经中有关Rust包的介绍,攥写本章的目的是为了更好的掌握rust中包和模块的关系以及它们的代码组织方式,方便于未来的项目开发。 Rust中代码组织相关概念 在Rust中,代码的组织大致可以分为四个层次: 项目(Package) 工作空间(Workspace) 包(Crate) 模块(Module) Package 项目 Package其实就是通过命令cargo new创建的项目文件,其显著特征便是包含了cargo.toml文件,该文件标注了该Package的一些基本信息:例如名称,版本号,依赖等等。 一个Package由一个或多个包(Crate)组成,但是它最多只能包含一个库类型的包(即名为lib.rs的文件)。 Package还可以分为二进制Package和库Package。 二进制Package 直接使用命令cargo new package-name的Package-项目即为二进制项目,虽然在cargo.toml中没有显示指出Package的入口文件,但Cargo的惯例是:src/main.rs即为二进制包的根文件,即入口文件,所有的代码的执行都是从src/main.rs中的fn main()中开始执行的。 输入cargo run可以直接编译运行。 库Package 库Package在创建时需要增加--lib命令行参数选项,即cargo new package-lib-name --lib在,这样获得的Package是一个库Package,它只能作为一个第三方库被其他项目引用,而不能单独编译运行。 与 src/main.rs 一样,Cargo 知道,如果一个 Package 包含有 src/lib.rs,意味它包含有一个库类型的同名包 my-lib,该包的根文件是 src/lib.rs。 Package文件结构 需要注意的是,main.rs和lib.rs不是互斥关系,二者是可以共存的。当二者共存时,那就意味着它包含两个包:库包和二进制包,这两个包名也都是 package-name —— 都与 Package 同名。 下面是一个Package的文件结构。 . ├── Cargo.toml ├── Cargo.lock ├── src │ ├── main.rs │ ├── lib.rs │ └── bin │ └── main1.rs │ └── main2.rs ├── tests │ └── some_integration_tests.rs ├── benches │ └── simple_bench....

July 5, 2023 · 2 min · 302 words · Runtus