Node.js的模块加载方法
•JavaScript 现在是两种模块,一种是ES6模块,简称ESM,另一种是CommonJS模块,简称CJS。
•Node.js 要求ES6模块采用 .mjs 后缀名,也就是说,只要脚本文件里面使用import 或者 export 命令,
那么就必需采用.mjs后缀名。Node.js遇到 .mjs 文件,就认为它是ES6模块,默认启用严格模式,不必再
每个模块文件顶部指定 use strict。
•如果不希望将后缀名改变.mjs,可以在项目的 package.json 文件中,指定 type 字段为 module。
{ "type": "module" }
•一旦设置之后,改目录里面的JS脚本,就被解释用ES6模块。
# 解释成 ES6 模块
$ node my-app.js
•如果这是要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成 .cjs,如果没有 type 字段,
或者 type 字段为 commonjs,则 .js 脚本会被解释成 CommonJS 模块。
•总结:
–.mjs 文件总是以 ES6 模块加载
–.cjs 文件总是以 CommonJS 模块加载
–.js 文件的加载取决于 package.json 里面的 type 字段的设置
•注意:
–ES6 模块与 CommonJS 模块尽量不要混用
–require 命令不能加载 .mjs 文件会报错,只有 import 命令才可以加载 .mjs 文件
–.mjs 文件里面也不能使用 require 命令,必须使用 import
package.json 的 main 字段
•package.json 文件有两个字段可以指定模块的入口文件,main和exports。
// ./node_modules/es-module-package/package.json { "type": "module", "main": "./src/index.js" }
// 它的格式为 ES6 模块。如果没有type字段,index.js就会被解释为 CommonJS 模块。
package.json 的 exports 字段
•exports 字段的优先级高于 main 字段。
•子目录别名
–package.json 文件的 字段可以指定脚本或子目录的别名。
// ./node_modules/es-module-package/package.json { "exports": { "./features/": "./src/features/" } } import feature from 'es-module-package/features/x.js'; // 加载 ./node_modules/es-module-package/src/features/x.js
•main 的别名
–exports 字段的别名如果是.,就代表模块的主入口,优先级高于 main 字段,并且可以
直接简写成 exports 字段的值。
{ "exports": { ".": "./main.js" } } // 等同于 { "exports": "./main.js" } // 由于exports字段只有支持 ES6 的 Node.js 才认识,所以可以用来兼容旧版本的 Node.js。 { "main": "./main-legacy.cjs", "exports": { ".": "./main-modern.cjs" } }
•条件加载
–利用.这个别名,可以为 ES6 模块和 CommonJS 指定不同的入口。目前,这个功能需要在 Node.js
–运行的时候,打开 --experimental-conditional-exports 标志。
加载路径
•ES6 模块的加载路径必须给出脚本的完整路径,不能省略脚本的后缀名,import 命令和 package.json
文件的 main 字段如果省略脚本的后缀名,会报错。
// ES6 模块中将报错 import { something } from './index';
•为了与浏览器的import加载规则相同,Node.js 的.mjs文件支持 URL 路径。
import'./foo.mjs?query=1';// 加载 ./foo 传入参数 ?query=1
•上面代码中,脚本路径带有参数?query=1,Node 会按 URL 规则解读。同一个脚本只要参数不同,就会被加载多次,
•并且保存成不同的缓存。由于这个原因,只要文件名中含有: 、%、#、? 等特殊字符,最好对这些字符进行转义。
•目前,Node.js 的import命令只支持加载本地模块(file:协议)和 data: 协议,不支持加载远程模块。
内部变量
•首先,this 关键字。ES6模块之中,顶层的 this 指向 undefined, CommonJS 模块的顶层 this 指向
当前的模块,这是两者的一个重大差异。
CommonJS 模块的加载原理
•CommonJS的一个模块,就是一个脚本文件,require 命令第一次加载该脚本,就会执行整个脚本,然后在内存
中生成一个对象。
{ id: '...', exports: { ... }, loaded: true, ... }
•上面代码就是 Node 内部加载模块后生成的一个对象。该对象的id属性是模块名,exports 属性是模块输出的各个
接口,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。
•以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,
而是到缓存之中取值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,
就返回第一次运行的结果,除非手动清除系统缓存。
1.编程风格
块级作用域
•(1)let 取代 var
•ES6 提出了两个新的声明变量的命令:let 和const。其中, let 完全可以取代var,因为两者语义相同,
而且 let 没有副作用。
•var命令存在变量提升效用,let命令没有这个问题。
•(2)全局常量和线程安全
•在let和const之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量。
•const 优于 let 的几个原因:
–一个是const可以提醒阅读程序的人,这个变量不应该改变;
–另一个是const比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;
–最后一个原因是 JavaScript 编译器会对const进行优化,所以多使用const,有利于提高程序的运行效率,
也就是说let和const的本质区别,其实是编译器内部的处理不同
–阅读代码的人立刻会意识到不应该修改这个值
–防止了无意间修改变量值所导致的错误
字符串
•静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。
// bad const a = "foobar"; const b = 'foo' + a + 'bar'; // acceptable const c = `foobar`; // good const a = 'foobar'; const b = `foo${a}bar`;
解构赋值
对象
•单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。
// bad const a = { k1: v1, k2: v2, }; const b = { k1: v1, k2: v2 }; // good const a = { k1: v1, k2: v2 }; const b = { k1: v1, k2: v2, };
•对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用 Object.assign 方法。
// bad const a = {}; a.x = 3; // if reshape unavoidable const a = {}; Object.assign(a, { x: 3 }); // good const a = { x: null }; a.x = 3;
•如果对象的属性名是动态的,可以在创造对象的时候,使用属性表达式定义。
// bad const obj = { id: 5, name: 'San Francisco', }; obj[getKey('enabled')] = true; // good const obj = { id: 5, name: 'San Francisco', [getKey('enabled')]: true, };
•另外,对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写。
var ref = 'some value'; // bad const atom = { ref: ref, value: 1, addValue: function (value) { return atom.value + value; }, }; // good const atom = { ref, value: 1, addValue(value) { return atom.value + value; }, };
数组
•使用扩展运算符 (...) 拷贝数组。
// bad const len = items.length; const itemsCopy = []; let i; for (i = 0; i << span=""> len; i++) { itemsCopy[i] = items[i]; } // good const itemsCopy = [...items];
•使用 Array.from 方法,将类似数组的对象转为数组。
const foo = document.querySelectorAll('.foo'); const nodes = Array.from(foo);
函数
•立即执行函数可以写成箭头函数的形式。
(() => { console.log('Welcome to the Internet.'); })();
•那些使用匿名函数当作参数的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了 this。
// bad [1, 2, 3].map(function (x) { return x * x; }); // good [1, 2, 3].map((x) => { return x * x; }); // best [1, 2, 3].map(x => x * x);
•箭头函数取代Function.prototype.bind,不应再用 self/_this/that 绑定 this。
// bad const self = this; const boundMethod = function(...params) { return method.apply(self, params); } // acceptable const boundMethod = method.bind(this); // best const boundMethod = (...params) => method.apply(this, params);
•简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。
•所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。
// bad function divide(a, b, option = false ) { } // good function divide(a, b, { option = false } = {}) { }
•不要在函数体内使用 arguments 变量,使用 rest 运算符(...)代替。因为 rest 运算符显式表明
你想要获取参数,而且 arguments 是一个类似数组的对象,而 rest 运算符可以提供一个真正的数组。
// bad function concatenateAll() { const args = Array.prototype.slice.call(arguments); return args.join(''); } // good function concatenateAll(...args) { return args.join(''); } •使用默认值语法设置函数参数的默认值。 // bad function handleThings(opts) { opts = opts || {}; } // good function handleThings(opts = {}) { // ... }
Map结构
•注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value
的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。
let map = new Map(arr); for (let key of map.keys()) { console.log(key); } for (let value of map.values()) { console.log(value); } for (let item of map.entries()) { console.log(item[0], item[1]); }
Class
•总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解。
// bad function Queue(contents = []) { this._queue = [...contents]; } Queue.prototype.pop = function() { const value = this._queue[0]; this._queue.splice(0, 1); return value; } // good class Queue { constructor(contents = []) { this._queue = [...contents]; } pop() { const value = this._queue[0]; this._queue.splice(0, 1); return value; } }
•使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险。
// bad const inherits = require('inherits'); function PeekableQueue(contents) { Queue.apply(this, contents); } inherits(PeekableQueue, Queue); PeekableQueue.prototype.peek = function() { return this._queue[0]; } // good class PeekableQueue extends Queue { peek() { return this._queue[0]; } }
模块
•ES6 模块语法是 JavaScript 模块的标准写法,坚持使用这种写法,取代 Node.js 的 CommonJS 语法。
•首先,使用import取代require()。
// CommonJS 的写法 const moduleA = require('moduleA'); const func1 = moduleA.func1; const func2 = moduleA.func2; // ES6 的写法 import { func1, func2 } from 'moduleA';
•其次,使用export取代module.exports。
// commonJS 的写法 var React = require('react'); var Breadcrumbs = React.createClass({ render() { return << span="">nav />; } }); module.exports = Breadcrumbs; // ES6 的写法 import React from 'react'; class Breadcrumbs extends React.Component { render() { return << span="">nav />; } }; export default Breadcrumbs;
•如果模块只有一个输出值,就使用export default,如果模块有多个输出值,除非其中某个输出值特别重要,
否则建议不要使用export default,即多个输出值如果是平等关系,export default与普通的export就不要同时
使用。
•如果模块默认输出一个函数,函数名的首字母应该小写,表示这是一个工具方法。
function makeStyleGuide() { } export default makeStyleGuide;
•如果模块默认输出一个对象,对象名的首字母应该大写,表示这是一个配置值对象
const StyleGuide = { es6: { } }; export default StyleGuide;
ESLint的使用
•ESLint 是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。
// 首先,在项目的根目录安装 ESLint。 $ npm install --save-dev eslint // 然后,安装 Airbnb 语法规则,以及 import、a11y、react 插件。 $ npm install --save-dev eslint-config-airbnb $ npm install --save-dev eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react // 最后,在项目的根目录下新建一个.eslintrc文件,配置 ESLint。 { "extends": "eslint-config-airbnb" }
管道运算符
•JavaScript 的管道是一个运算符,写作|>。它的左边是一个表达式,右边是一个函数。
管道运算符把左边表达式的值,传入右边的函数进行求值。
eg1:
x |> f // 等同于 f(x)
eg2:
// 传统的写法 function doubleSay (str) { return str + ", " + str; } function capitalize (str) { return str[0].toUpperCase() + str.substring(1); } function exclaim (str) { return str + '!'; } // 传统的写法 exclaim(capitalize(doubleSay('hello'))) // "Hello, hello!" // 管道的写法 'hello' |> doubleSay |> capitalize |> exclaim // "Hello, hello!"
•管道运算符只能传递一个值,这意味着它右边的函数必须是一个单参数函数。如果是多参数函数,就必须进行柯里化,
改成单参数的版本。
function double (x) { return x + x; } function add (x, y) { return x + y; } let person = { score: 25 }; person.score |> double |> (_ => add(7, _)) // 57
冒号运算符
•箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(call、apply、bind)。但是,
箭头函数并不适用于所有场合,所以现在有一个提案,提出了“函数绑定”(function bind)运算符,
用来取代call、apply、bind调用。
•函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,
作为上下文环境(即this对象),绑定到右边的函数上面。
eg1:
foo::bar; // 等同于 bar.bind(foo); foo::bar(...arguments); // 等同于 bar.apply(foo, arguments); const hasOwnProperty = Object.prototype.hasOwnProperty; function hasOwn(obj, key) { return obj::hasOwnProperty(key); }
•如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。
eg2:
var method = obj::obj.foo; // 等同于 var method = ::obj.foo; let log = ::console.log; // 等同于 var log = console.log.bind(console);
•如果双冒号运算符的运算结果,还是一个对象,就可以采用链式写法。
eg3:
import { map, takeWhile, forEach } from "iterlib"; getPlayers() ::map(x => x.character()) ::takeWhile(x => x.strength > 100) ::forEach(x => console.log(x));
注:如果想获取全文pdf文件,关注【前端进阶圈】发送 ES6 即可获取哟。