标签:sha tar 同名 设置 私有 外部 lock 赋值 lex
非模块化开发的问题
- 命名冲突
- 文件依赖高
- 可扩展性低
- 可重用性低
- 等等......
从最简单的加减乘除运算来举例说明,为了方便理解这里都没有采用ES6的语法。
// 早期的开发过程中就是将重复使用的代码封装到函数中
// 再将一系列的函数放到一个文件中,称之为模块
// 缺点:存在命名冲突,可维护性也不高的问题
// 仅仅从代码角度来说:没有任何模块的概念
function convertor(a) {
return parseFloat(a);
}
function add(a, b) {
return convertor(a) + convertor(b);
}
// 有了传统编程语言中的命名空间
// 从代码层面就已经有了模块的感觉
// 避免了多处全局污染
// 缺点:没有私有空间,没有抽离私有成员
var calculator = {
add: function (a, b) {
return this.convertor(a) + this.convertor(b);
},
convertor:function(a){
return parseFloat(a)
}
};
// 这里形成一个单独的私有的空间
// 高内聚,低耦合:模块内部相关性强,模块之间没有过多相互牵连,如convertor和add
// 缺点:可扩展性低
var calculator = (function () {
// 将一个成员私有化,外部无法访问和修改
function convertor(a) {
return parseFloat(a);
}
// 抽象公共方法(其他成员中都会用到的)
function add(a, b) {
return convertor(a) + convertor(b);
}
return {
add:add
}
})();
// calc_v2016.js
(function (window,calculator) {
function convert(input) {
return parseFloat(input);
}
calculator = {
add: function (a, b) {
return convert(a) + convert(b);
}
}
window.calculator = calculator;
})(window, {});
// 新增需求 remain
// calc_v2017.js
// 开闭原则:对新增开放,对修改关闭
(function (calculator) {
function convert(input) {
return parseInt(input);
}
// calculator 如果存在的话,我就是扩展,不存在我就是新加
calculator.remain = function (a, b) {
return convert(a) % convert(b);
}
window.calculator = calculator;
})(window.calculator || {});
// calc_v2016.js
(function (window,calculator) {
//对全局产生依赖,不能这样用
console.log(document);
function convert(input) {
return parseFloat(input);
}
calculator = {
add: function (a, b) {
return convert(a) + convert(b);
}
}
window.calculator = calculator;
})(window, {});
// 新增需求
// calc_v2017.js
(function (calculator,document) {
// 依赖函数的参数,是属于模块内部
console.log(document);
function convert(input) {
return parseInt(input);
}
calculator.remain = function (a, b) {
return convert(a) % convert(b);
}
window.calculator = calculator;
})(window.calculator || {},document);
以上通过一些简短的代码介绍了模块化发展大致情况。
export
和 import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。在这里就不具体展示每种规范的具体写法了,详情请点击阮一峰《ES6 入门教程》。Node 采用的模块化结构是按照 CommonJS 规范
CommonJS 模块的特点
module.exports
不会再次执行该模块。模块的分类
模块的定义
module.id
?模块的识别符,通常是带有绝对路径的模块文件名。module.filename
?模块定义的文件的绝对路径。module.loaded
?返回一个布尔值,表示模块是否已经完成加载。module.parent
?返回一个对象,表示调用该模块的模块。module.children
?返回一个数组,表示该模块要用到的其他模块。module.exports
?表示模块对外输出的值。// 导出方式,`module.exports` 和 `exports`
exports.name = value;
module.exports = { name: value };
module.exports
是用于为模块导出成员的接口;
exports
是指向 module.exports
的别名,相当于在模块开始的时候执行:var exports = module.exports
。
用Node手写一个简单的 require
function $require(files) {
const fs = require(‘fs‘);
const path = require(‘path‘);
// 注意,这里实现的缓存不是Node的缓存机制
const filename = path.join(__dirname, files);
$require.cache=$require.cache||{};
if($require.cache[filename]) return $require.cache[filename].exports;
const dirname=path.dirname(filename);
const file = fs.readFileSync(filename);
const module = {
id:filname,
exports: {}
};
// 保存module.exports重新赋值前的值
const { exports } = module;
const code = `
(function (module,exports,__dirname,__filename) {
${file}
})(module,exports,dirname,filename)
`;
eval(code);
$require.cache[filename]=module;
return module.exports;
}
从以上代码我们可以知道:
module.exports
都是缓存哪怕这个js
还没执行完毕(因为先加入缓存后执行模块)。return
这个变量的其实跟a = b
赋值一样, 基本类型导出的是值, 引用类型导出的是指针(内存地址)。exports
和module.exports
持有相同引用,因为最后导出的是module.exports
,所以对exports
进行赋值会导致exports
操作的不再是module.exports
的引用。Node 使用 CommonJS 模块规范,内置的 require 函数用于加载模块文件。require 的基本功能是,读入并执行一个 javascript 文件,然后返回该模块的 exports 对象。 如果没有发现指定模块,会报错。
require 加载文件规则如下:
require(‘../file.js‘); // 上级目录下找 file.js 文件
require(‘./file.js‘); // 同级目录找 file.js 文件
require(‘file.js‘); // 同级目录找 file.js 文件
require(‘/vue-template/src/main.js‘); // 以绝对路径的方式找
../
或./
或/
开头,则表示加载的是一个默认提供的核心模块(位于 Node 的系统安装目录 node_modules)中// lib.js
const counter = 3;
const incCounter = () => {
counter++;
};
module.exports = {
counter,
incCounter,
};
// main.js
const mod = require(‘./lib‘);
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
// lib.js
export let counter = 3;
export const incCounter = () => {
counter++;
};
// main.js
import { counter, incCounter } from ‘./lib‘;
console.log(counter); // 3
incCounter();
console.log(counter); // 4
JS 引擎对脚本静态分析的时候,遇到模块加载命令import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,原始值变了,import
加载的值也会跟着变。再举一例子:// m1.js
export var foo = ‘bar‘;
setTimeout(() => foo = ‘baz‘, 500);
// m2.js
import {foo} from ‘./m1.js‘;
console.log(foo); // bar
setTimeout(() => console.log(foo), 500); // baz
上面代码表明,ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。module.exports
属性),该对象只有在脚本运行完才会生成,然后再从这个对象上面读取相关方法,这种加载称为运行时加载。export
命令显式指定输出的代码,import
时采用静态命令的形式,即在代码静态解析阶段就会生成,而不是加载整个模块,这种加载称为编译时输出。这也是ES6 模块非常轻松的实现Tree Shaking
的重要因素。import
被导入的变量是只读的,不能被重新赋值。import
会自动提升到代码的顶层,以下代码都会报错:const num = 100;
import xxx from ‘xxx-module‘;
// if for while ...
if(boolExp){
import xxx from ‘xxx-module‘;
}
因为,CommonJS 模块是动态语法可以写在判断里,ES6 模块静态语法只能写在顶层。this
指向当前模块,ES6 模块的顶层作用域里 this
指向 undefined
。// a.js
module.exports.a = 1;
var b = require(‘./b‘);
console.log(b);
module.exports.a = 2;
// b.js
module.exports.b = 11;
var a = require(‘./a‘);
console.log(a);
module.exports.b = 22;
//main.js
var a = require(‘./a‘);
console.log(a);
运行此段代码结合上面的require demo
,分析每一步过程:
执行 node main.js -> 第一行 require(a.js)
,(node
执行也可以理解为调用了require方法,我们省略require(main.js)
内容)进入 require(a)方法: 判断缓存(无) -> 初始化一个 module -> 将 module 加入缓存 -> 执行模块 a.js 内容
,(需要注意 是先缓存, 后执行模块内容)a.js: 第一行导出 a = 1 -> 第二行 require(b.js)
(a 只执行了第一行)进入 require(b) 内 同 1 -> 执行模块 b.js 内容
b.js: 第一行 b = 11 -> 第二行 require(a.js)
require(a) 此时 a.js 是第二次调用 require -> 判断缓存(有)-> cachedModule.exports -> 回到 b.js
(因为js
对象引用问题 此时的 cachedModule.exports = { a: 1 }
)b.js:第三行 输出 { a: 1 } -> 第四行 修改 b = 22 -> 执行完毕回到 a.js
a.js:第二行 require 完毕 获取到 b -> 第三行 输出 { b: 22 } -> 第四行 导出 a = 2 -> 执行完毕回到 main.js
main.js:获取 a -> 第二行 输出 { a: 2 } -> 执行完毕
。// bar.js
import { foo } from ‘./foo‘;
console.log(foo);
export const bar = ‘bar‘;
// foo.js
import { bar } from ‘./bar‘;
console.log(bar);
export const foo = ‘foo‘;
// main.js
import { bar } from ‘./bar‘;
console.log(bar);
执行 main.js -> 导入 bar.js
bar.js -> 导入 foo.js
foo.js -> 导入 bar.js -> bar.js 已经执行过(它认为这个接口已经存在了,就不会再去执行) -> 输出 bar -> bar is not defined, bar 未定义报错
。function
的方式解决:// bar.js
import { foo } from ‘./foo‘;
console.log(foo());
export function bar(){
return ‘bar‘;
}
// foo.js
import { bar } from ‘./bar‘;
console.log(bar());
export function foo(){
return ‘foo‘;
}
// main.js
import { bar } from ‘./bar‘;
console.log(bar());
这是因为函数具有提升作用,在执行import { foo } from ‘./foo‘
时,函数bar
就已经有定义了,所以foo.js
加载的时候不会报错。这也意味着,如果把函数foo
改写成函数表达式,也会报错。Node 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块 和 CommonJS 模块采用各自的加载方案。从 v13.2 版本开始,Node 已经默认打开了 ES6 模块支持。在此版本之前,想要在 Node 中使用 ES6 模块,需要添加--experimental-modules
,如:node --experimental-modules ./index.mjs
。
Node 要求使用 ES6 模块需要采用.mjs
后缀文件名。也就是说,Node 遇到.mjs
文件,就认为它是ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"
。
如果不希望将后缀名改成.mjs
,可以在项目的package.json
文件中,指定type
字段为module
。如果不希望将后缀名改成.mjs
,可以在项目的package.json
文件中,指定type
字段为module
。一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 Module。
{
"type": "module" // 开启 ES6 Module 模式
}
如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 模块脚本的后缀名都改成.cjs
。如果没有type
字段,或者type
字段为commonjs
,则.js
脚本会被解释成 CommonJS 模块。
package.json
文件有两个字段可以指定模块的入口文件:main
和exports
。
main
字段main
字段,指定模块加载的入口文件。{
"type": "module",
"main": "./index.js"
}
上面代码指定项目的入口脚本为./index.js
,它的格式为 ES6 模块。如果没有type
字段,index.js
就会被解释为 CommonJS 模块。exports
字段的优先级高于main
字段。它有多种用法。package.json
文件的exports
字段可以指定脚本或子目录的别名。// package.json
{
"exports": {
"./xxx-file-name": "./xxx-dir/xxx.js", // 指定脚本别名
"./xxx-dir-name/": "./xxx-dir/", // 指定子目录别名
}
}
// 模块引入
import module1 from ‘project-name/xxx-file-name‘;
import module2 from ‘project-name/xxx-dir-name/xxx.js‘;
(2)main 字段的别名exports
字段的别名如果是.
,就代表模块的主入口,优先级高于main
字段,并且可以直接简写成exports
字段的值。{
"exports": {
".": "./main.js"
}
}
// 等同于
{
"exports": "./main.js"
}
由于exports
字段只有支持 ES6 的 Node 才认识,所以可以用来兼容旧版本的 Node。{
"main": "./old-version.js",
"exports": {
".": "./new-version.js"
}
}
(3)条件加载.
这个别名,可以为 ES6 Module 和 CommonJS 模块指定不同的入口。目前,这个功能需要在 Node 运行的时候,打开--experimental-conditional-exports
标志。{
"type": "module",
"exports": {
".": {
"require": "./main.cjs", // require 规定 CommonJS 模块的入口
"default": "./main.js" // default 规定 `default` 条件指定其他情况的入口,即 ES6 模块的入口
}
}
}
上面可以简写如下:{
"exports": {
"require": "./main.cjs",
"default": "./main.js"
}
}
如果有别名则只能如下:{
"exports": {
".": {
"./xxx-file-name": "./xxx-dir/xxx.js",
"require": "./main.cjs",
"default": "./main.js"
}
}
}
有了上一节的条件加载以后,Node 本身就可以同时处理两种模块。
{
"type": "module",
"main": "./index.cjs",
"exports": {
"require": "./index.cjs",
"default": "./wrapper.mjs"
}
}
注意,import
命令加载 CommonJS 模块,只能整体加载,不能只加载单一的输出项。
// 正确
import packageMain from ‘commonjs-package‘;
// 报错
import { method } from ‘commonjs-package‘;
CommonJS 模块的require
命令不能加载ES6 模块,会报错,只能使用import()
这个方法加载。
Node 的内置模块可以整体加载,也可以加载指定的输出项。
以上就是我对模块化加载的理解,当然这里没有去过多的写案例,目的是为了以后回来查阅方便,方便自己记忆。
标签:sha tar 同名 设置 私有 外部 lock 赋值 lex
原文地址:https://www.cnblogs.com/onebox/p/13093752.html