require细节
require是个函数,可以帮助我们引入一个文件(模块)中导出的对象。
require的查找规则是什么:完整的文档很多,这里说下常见的规则。
- 情况一:X是一个核心模块,如path,http... 则直接返回核心模块,并且停止查找。
- 情况二:X是以./或../或/(根目录)开头,说明是本地文件,会从本地目录开始查找。
第一步:将X当作一个文件在对应的目录下查找对应的文件。
如果没有后缀名,会按如下顺序查找:文件X,X.js文件,X.json文件,X.node文件。
第二步:没有找到对应的文件,将X作为一个目录。
查找目录下的index文件,查找X/index.js,X/index.json和X/index.node。
如果都没有找到,那就报错:not found。
情况三:直接是一个X(没有路径),并且X不是核心模块。
先查找X是不是核心模块,若无,则在paths里面按顺序查询node_modules。
这个paths是module对象的一个属性module.paths。
这里不是说我们不需要加后缀名,而是node/webpack帮我们做了一个拼接,所以不加也可以生效。
一.模块的加载过程
- 模块在被第一次引入时,模块中的js代码会被运行一次。实际上就是按当前文件的代码顺序来执行。
- 模块被多次引入时会被缓存,最终只加载(运行)一次。
- 每个模块对象的module都有一个属性叫loaded。
- loaded为false表示还未加载,为true表示已被加载。
// bar.js
let name = "wwq";
console.log(name);
name = "11111";
console.log(name);
// foo.js
require('./bar');
// main.js
require('./foo');
console.log('main中被执行');
// 结果
wwq
11111
main中被执行
-
循环引用的加载顺序是什么
实际上就是图结构的遍历,这里是深度遍历,也就是广度优先(数据结构基础)。
二.cjs规范的缺点
- cjs加载模块是同步的;
- 同步也就是说只有对应的模块加载完毕,当前模块中的内容才能被运行。在服务器的时候影响不大,但在浏览器中就有影响了。
- 浏览器加载js文件需要先从服务器将文件下载下来,之后再加在运行。那么采用同步也就意味着后续的js代码无法正常运动,即使是一些简单的dom操作。
- 所以在浏览器中我们通常不用cjs,但在webpack中使用cjs是另外的事情,因为它会将我们的代码转成浏览器可以直接执行的代码。
- 早期为了在浏览器中使用模块化,通常会采用AMD或CMD,但是现在浏览器已经支持了ESM,另一方面借助于webpack等工具可以实现对cjs/esm代码的转换。AMD和CMD使用已经很少了,但也做个实例。
三.AMD规范(了解一下
AMD是应用于浏览器的一种模块化规范:
- 是Asynchronous Module Definition(异步模块定义)的缩写。
- 采用的是异步加载模块。
-
事实上AMD要早于cjs,但是现在使用已经很少了。
AMD的实现,常用的库有require.js,curl.js。
// 代码结构
// index.html
<script src="./lib/require.js" data-main="./index.js"></script>
// index.js
(function () {
require.config({
baseUrl: "",
paths: {
bar: "./modules/bar",
foo: "./modules/foo",
},
});
require(["foo"], function (foo) {});
})();
// modules/bar.js
define(function () {
const name = "coderwwq";
const age = 18;
const sayHello = function (name) {
console.log("你好", name);
};
return {
name,
age,
sayHello,
};
});
// modules/foo.js
define(["bar"], function (bar) {
console.log(bar.name);
console.log(bar.age);
bar.sayHello("12312wqwq");
});
四.CMD规范(了解一下
CMD也是应用于浏览器的一种模块化规范:
- 是Common Module Definition(异步模块定义)的缩写。
- 采用的是异步加载模块,拥有cjs的有点,现在使用也很少了。
- 优秀的实现方案,SeaJS。
<script src="./lib/sea.js"></script>
<script>
// 核心代码
seajs.use("./index.js")
</script>
// index.js
define(function(require, exports, module) {
const foo = require('./modules/foo');
console.log(foo.name);
console.log(foo.age);
foo.sayHello('123123');
})
// ./modules/foo.js
define(function(require, exports, module) {
const name="wwq";
const age = 20;
const sayHello = function(name) {
console.log("你好", name);
}
module.exports = {
name,
age,
sayHello,
}
})
五.ES Module规范
ES Module和CommonJS的模块化有一些不同之处。
- ESM使用了import和export关键字(解析的时候,交给js引擎对关键字进行解析)。
- 另一方面采用编译期的静态分析,并且也加入了动态引用的方式。
- 使用ES Module将会自动开启严格模式,use strict。
浏览器演示ES6模块化开发
type="module"表示这个模块是异步加载.
// index.html
<script type="module" src="./index.js"></script>
// index.js
console.log('hello 123');
但是这些么写的话会有这个跨域的报错。
原因是:出于js模块安全性需要,通过本地加载html文件(比如file://,不支持file协议)时,会出现CORS错误。
解决方法:需要通过一个服务器来测试。在vscode里面有个插件叫「live server」,这个插件会开启一个本地服务,并且对我们的代码进行热更新。
常见导出方式有三种
// 1. 方式一:
export const name = "wwq";
export const age = 20;
export const sayHello = function (name) {
console.log("你好", name);
};
// 2. 方式二:大括号内统一导出,但不是一个对象,
// { 大括号内放置要导出的变量的引用列表 }
export { name, age, sayHello };
// 3. 方式三:{}导出时,可以给变量起别名
export { name as fname, age as fAge, sayHello as fSayhello };
常见导入方式有三种
// 常见的导入方式
// 方式一:普通导入
import { name, age, sayHello } from "./modules/foo.js";
// 方式二:导出变量之后可以起别名
import {
fName as wName,
fAge as wAge,
fSayHello as wSayHello,
} from "./modules/foo";
// 方式三:* as foo
import * as foo from "./modules/foo.js";
Export和Import结合使用
将希望暴露的所有接口放到一个文件中, 方便指定统一的接口规范, 也方便阅读.
// index.js
export { sum as barSum, reduce as barReduce } from './foo.js'
// foo.js
export { sum, reduce }
default用法
上面的一些代码示例是具名导出(named exports).
- 这是因为在export的时候指定了名字.
- 通过default, 在导入的时候不需要使用{}, 并且可以自己来指定名字.
- 也方便我们和现有的cjs等规范互相操作.
- 一个模块里面只能有一个.
// 导出
export default function() {
console.log('格式化');
}
// 导入
import format from './modules/foo.js';
format();
补充
- import加载模块的时候, 不能放入逻辑代码中(esm在被js引擎解析的时候, 必须确定依赖关系, 属于解析时加载, 这个和cjs的require要区分开来)
- 因为require本质是一个函数, 属于运行时(webpack环境下).
let flag = true;
if(flag) {
import format from './modules/foo.js';
}
在纯ES Module环境下可以使用import()函数, 属于异步加载
- 大多数脚手架cli是基于webpack的, 所以可以使用import()函数, 在使用这个函数时, webpack会对相应的模块单独打包到一个js文件中, 有助于首屏渲染.
const promise = import('./modules/foo.js').then(res => {
clg(res.name);
clg(res.age);
}
下面表示了esm的引用图, 具体就不细说了, 有疑问的同学可以留言.
五.Node对ESModule的支持
该文件是js文件, 默认情况下一个js文件就是一个模块, 但我们这里指的模块是cjs模块, 并不是es6的模块, 所以不被当成一个esmodule.
上图Warning提示我们需要在package.json里添加“type”: “module”, 或者使用.mjs作为文件的拓展名.
但是加了之后还不可以运行, 这回提示我们没有找到模块. 这是因为之前导入的时候拓展名是foo.js而不是foo.mjs, 改正之后就可以正常运行了.
五.Node对ESModule的支持
结论一:通常情况下,cjs不能还在esm
- cjs是同步加载的,但esm必须经过静态分析等,无法在这个时候执行js代码;
- 但这个不绝对,某些平台在实现的时候可以对代码进行针对性的解析,可能会支持;
- Node中不支持;