网站被降权表现,wordpress插件如何应用,河南住房和城乡建设厅门户网站,wordpress 美术前言 JavaScript 语言诞生至今#xff0c;模块规范化之路曲曲折折。 前言
JavaScript 语言诞生至今#xff0c;模块规范化之路曲曲折折。社区先后出现了各种解决方案#xff0c;包括 AMD、CMD、CommonJS 等#xff0c;而后 ECMA 组织在 JavaScript 语言标准层面#xff0… 前言 JavaScript 语言诞生至今模块规范化之路曲曲折折。 前言
JavaScript 语言诞生至今模块规范化之路曲曲折折。社区先后出现了各种解决方案包括 AMD、CMD、CommonJS 等而后 ECMA 组织在 JavaScript 语言标准层面增加了模块功能因为该功能是在 ES2015 版本引入的所以在下文中将之称为 ES6 module。 今天我们就来聊聊为什么会出现这些不同的模块规范它们在所处的历史节点解决了哪些问题
何谓模块化
或根据功能、或根据数据、或根据业务将一个大程序拆分成互相依赖的小文件再用简单的方式拼装起来。
全局变量
演示项目
为了更好的理解各个模块规范先增加一个简单的项目用于演示。
Window
在刀耕火种的前端原始社会JS 文件之间的通信基本完全依靠window对象借助 HTML、CSS 或后端等情况除外。 config.js var api https://github.com/ronffy;
var config {api: api,
}utils.js var utils {request() {console.log(window.config.api);}
}main.js window.utils.request();HTML !-- index.html --
!DOCTYPE html
html langenheadmeta charsetUTF-8meta title【深度全面】JS模块规范进化论/title
/headbody!-- 所有 script 标签必须保证顺序正确否则会依赖报错 --script src./js/config.js/scriptscript src./js/utils.js/scriptscript src./js/main.js/script
/body/html
IIFE
浏览器环境下在全局作用域声明的变量都是全局变量。全局变量存在命名冲突、占用内存无法被回收、代码可读性低等诸多问题。
这时IIFE匿名立即执行函数出现了
用 IIFE 重构 config.js
(function (root) {var api https://github.com/ronffy;var config {api: api,};root.config config;
}(window));
IIFE 的出现使全局变量的声明数量得到了有效的控制。
命名空间
依靠window对象承载数据的方式是 “不可靠” 的如window.config.api如果window.config不存在则window.config.api就会报错所以为了避免这样的错误代码里会大量的充斥var api window.config window.config.api;这样的代码。
这时namespace登场了简约版本的namespace函数的实现只为演示不要用于生产
function namespace(tpl, value) {return tpl.split(.).reduce((pre, curr, i) {return (pre[curr] i tpl.split(.).length - 1? (value || pre[curr]): (pre[curr] || {}))}, window);
}用namespace设置window.app.a.b的值
namespace(app.a.b, 3); // window.app.a.b 值为 3用namespace获取window.app.a.b的值
var b namespace(app.a.b); // b 的值为 3
var d namespace(app.a.c.d); // d 的值为 undefined app.a.c值为undefined但因为使用了namespace, 所以app.a.c.d不会报错变量d的值为undefined。
AMD
随着前端业务增重代码越来越复杂靠全局变量通信的方式开始捉襟见肘前端急需一种更清晰、更简单的处理代码依赖的方式将 JS 模块化的实现及规范陆续出现其中被应用较广的模块规范有 AMD。
面对一种模块化方案我们首先要了解的是1. 如何导出接口2. 如何导入接口。
AMD 异步模块定义规范AMD制定了定义模块的规则这样模块和模块的依赖可以被异步加载。这和浏览器的异步加载模块的环境刚好适应浏览器同步加载模块会导致性能、可用性、调试和跨域访问等问题。 本规范只定义了一个函数define它是全局变量。
/*** param {string} id 模块名称* param {string[]} dependencies 模块所依赖模块的数组* param {function} factory 模块初始化要执行的函数或对象* return {any} 模块导出的接口*/
function define(id?, dependencies?, factory): anyRequireJS
AMD 是一种异步模块规范RequireJS 是 AMD 规范的实现。
接下来我们用 RequireJS 重构上面的项目。
在原项目 js 文件夹下增加 require.js 文件 define(function() {var api https://github.com/ronffy;var config {api: api,};return config;});utils.js define([./config], function(config) {var utils {request() {console.log(config.api);}};return utils;
});require([./utils], function(utils) {utils.request();});html !-- index.html --
!-- ...省略其他 --
bodyscript data-main./js/main src./js/require.js/script
/body
/html可以看到使用 RequireJS 后每个文件都可以作为一个模块来管理通信方式也是以模块的形式这样既可以清晰的管理模块依赖又可以避免声明全局变量。
更多 AMD 介绍请查看文档。 更多 RequireJS 介绍请查看文档。
特别说明 先有 RequireJS后有 AMD 规范随着 RequireJS 的推广和普及AMD 规范才被创建出来。
CommonJS
前面说了 AMD 主要用于浏览器端随着 node 诞生服务器端的模块规范 CommonJS 被创建出来。
还是以上面介绍到的 config.js、utils.js、main.js 为例看看 CommonJS 的写法: config.js var api https://github.com/ronffy;
var config {api: api,
};
module.exports config;utils.js var config require(./config);
var utils {request() {console.log(config.api);}
};
module.exports utils;main.js var utils require(./utils);
utils.request();
console.log(global.api)执行node main.jshttps://github.com/ronffy被打印了出来。 在 main.js 中打印global.api打印结果是undefined。node 用global管理全局变量与浏览器的window类似。与浏览器不同的是浏览器中顶层作用域是全局作用域在顶层作用域中声明的变量都是全局变量而 node 中顶层作用域不是全局作用域所以在顶层作用域中声明的变量非全局变量。
module.exports 和 exports
我们在看 node 代码时应该会发现关于接口导出有的地方使用module.exports而有的地方使用exports这两个有什么区别呢?
CommonJS 规范仅定义了exports但exports存在一些问题下面会说到所以module.exports被创造了出来它被称为 CommonJS2 。 每一个文件都是一个模块每个模块都有一个module对象这个module对象的exports属性用来导出接口外部模块导入当前模块时使用的也是module对象这些都是 node 基于 CommonJS2 规范做的处理。
// a.js
var s i am ronffy
module.exports s;
console.log(module);执行node a.js看看打印的module对象
{exports: i am ronffy,id: ., // 模块idfilename: /Users/apple/Desktop/a.js, // 文件路径名称loaded: false, // 模块是否加载完成parent: null, // 父级模块children: [], // 子级模块paths: [ /* ... */ ], // 执行 node a.js 后 node 搜索模块的路径
}其他模块导入该模块时
// b.js
var a require(./a.js); // a -- i am ronffy当在 a.js 里这样写时
// a.js
var s i am ronffy
exports s;a.js 模块的module.exports是一个空对象。
// b.js
var a require(./a.js); // a -- {}把module.exports和exports放到 “明面” 上来写可能就更清楚了
var module {exports: {}
}
var exports module.exports;
console.log(module.exports exports); // truevar s i am ronffy
exports s; // module.exports 不受影响
console.log(module.exports exports); // false
模块初始化时exports和module.exports指向同一块内存exports被重新赋值后就切断了跟原内存地址的关系。
所以exports要这样使用
// a.js
exports.s i am ronffy;// b.js
var a require(./a.js);
console.log(a.s); // i am ronffyCommonJS 和 CommonJS2 经常被混淆概念一般大家经常提到的 CommonJS 其实是指 CommonJS2本文也是如此不过不管怎样大家知晓它们的区别和如何应用就好。
CommonJS 与 AMD
CommonJS 和 AMD 都是运行时加载换言之都是在运行时确定模块之间的依赖关系。
二者有何不同点
CommonJS 是服务器端模块规范AMD 是浏览器端模块规范。CommonJS 加载模块是同步的即执行var a require(./a.js);时在 a.js 文件加载完成后才执行后面的代码。AMD 加载模块是异步的所有依赖加载完成后以回调函数的形式执行代码。[如下代码]fs和chalk都是模块不同的是fs是 node 内置模块chalk是一个 npm 包。这两种情况在 CommonJS 中才有AMD 不支持。
var fs require(fs);
var chalk require(chalk);ES6 module
AMD 是在原有 JS 语法的基础上二次封装的一些方法来解决模块化的方案ES6 module在很多地方被简写为 ESM是语言层面的规范ES6 module 旨在为浏览器和服务器提供通用的模块解决方案。长远来看未来无论是基于 JS 的 WEB 端还是基于 node 的服务器端或桌面应用模块规范都会统一使用 ES6 module。
兼容性
目前无论是浏览器端还是 node 都没有完全原生支持 ES6 module如果使用 ES6 module 可借助 babel等编译器。本文只讨论 ES6 module 语法故不对 babel 或 typescript 等可编译 ES6 的方式展开讨论。
导出接口
CommonJS 中顶层作用域不是全局作用域同样的ES6 module 中一个文件就是一个模块文件的顶层作用域也不是全局作用域。导出接口使用export关键字导入接口使用import关键字。
export导出接口有以下方式 方式 1 export const prefix https://github.com;
export const api ${prefix}/ronffy; 方式 2 const prefix https://github.com;
const api ${prefix}/ronffy;
export {prefix,api,
}
方式 1 和方式 2 只是写法不同结果是一样的都是把prefix和api分别导出。 方式 3默认导出 // foo.js
export default function foo() {}// 等同于
function foo() {}
export {foo as default
}
export default用来导出模块默认的接口它等同于导出一个名为default的接口。配合export使用的as关键字用来在导出接口时为接口重命名。 方式 4先导入再导出简写 export { api } from ./config.js;
// 等同于
import { api } from ./config.js;
export {api
}
如果需要在一个模块中先导入一个接口再导出可以使用export ... from module这样的简便写法。
导入模块接口
ES6 module 使用import导入模块接口。
导出接口的模块代码 1
// config.js
const prefix https://github.com;
const api ${prefix}/ronffy;
export {prefix,api,
}接口已经导出如何导入呢 方式 1 import { api } from ./config.js;
// or
// 配合import使用的as关键字用来为导入的接口重命名。
import { api as myApi } from ./config.js; 方式 2整体导入 import * as config from ./config.js;
const api config.api;
将 config.js 模块导出的所有接口都挂载在config对象上。 方式 3默认导出的导入 // foo.js
export const conut 0;
export default function myFoo() {}
// index.js
// 默认导入的接口此处刻意命名为cusFoo旨在说明该命名可完全自定义。
import cusFoo, { count } from ./foo.js;// 等同于
import { default as cusFoo, count } from ./foo.js;
export default导出的接口可以使用import name from module导入。这种方式使导入默认接口很便捷。
方式 4整体加载
这样会加载整个 config.js 模块但未导入该模块的任何接口。
方式 5动态加载模块
上面介绍了 ES6 module 各种导入接口的方式但有一种场景未被涵盖动态加载模块。比如用户点击某个按钮后才弹出弹窗弹窗里功能涉及的模块的代码量比较重所以这些相关模块如果在页面初始化时就加载实在浪费资源import()可以解决这个问题从语言层面实现模块代码的按需加载。
ES6 module 在处理以上几种导入模块接口的方式时都是编译时处理所以import和export命令只能用在模块的顶层以下方式都会报错
// 报错
if (/* ... */) {import { api } from ./config.js;
}// 报错
function foo() {import { api } from ./config.js;
}// 报错
const modulePath ./utils /api.js;
import modulePath;
使用import()实现按需加载
function foo() {import(./config.js).then(({ api }) {});
}const modulePath ./utils /api.js;
import(modulePath);
CommonJS 和 ES6 module
CommonJS 和 AMD 是运行时加载在运行时确定模块的依赖关系。 ES6 module 是在编译时import()是运行时加载处理模块依赖关系。
CommonJS
CommonJS 在导入模块时会加载该模块所谓 “CommonJS 是运行时加载”正因代码在运行完成后生成module.exports的缘故。当然CommonJS 对模块做了缓存处理某个模块即使被多次多处导入也只加载一次。
// o.js
let num 0;
function getNum() {return num;
}
function setNum(n) {num n;
}
console.log(o init);
module.exports {num,getNum,setNum,
}
// a.js
const o require(./o.js);
o.setNum(1);
// b.js
const o require(./o.js);
// 注意此处只是演示项目里不要这样修改模块
o.num 2;
// main.js
const o require(./o.js);require(./a.js);
console.log(a o.num:, o.num);require(./b.js);
console.log(b o.num:, o.num);
console.log(b o.getNum:, o.getNum());
命令行执行node main.js打印结果如下
o init 模块即使被其他多个模块导入也只会加载一次并且在代码运行完成后将接口赋值到module.exports属性上。a o.num: 0 模块在加载完成后模块内部的变量变化不会反应到模块的module.exports。b o.num: 2 对导入模块的直接修改会反应到该模块的module.exports。b o.getNum: 1 模块在加载完成后即形成一个闭包。
ES6 module
// o.js
let num 0;
function getNum() {return num;
}
function setNum(n) {num n;
}
console.log(o init);
export {num,getNum,setNum,
}
// main.js
import { num, getNum, setNum } from ./o.js;console.log(o.num:, num);
setNum(1);console.log(o.num:, num);
console.log(o.getNum:, getNum());
我们增加一个 index.js 用于在 node 端支持 ES6 module
// index.js
require(babel/register)({presets: [babel/preset-env]
});module.exports require(./main.js)
命令行执行npm install babel/core babel/register babel/preset-env -D安装 ES6 相关 npm 包。
命令行执行node index.js打印结果如下
o init 模块即使被其他多个模块导入也只会加载一次。o.num: 0o.num: 1 编译时确定模块依赖的 ES6 module通过import导入的接口只是值的引用所以num才会有两次不同打印结果。o.getNum: 1
对于打印结果 3知晓其结果在项目中注意这一点就好。这块会涉及到 “Module Records模块记录”、“module instance模快实例” “linking链接” 等诸多概念和原理
ES6 module 是编译时加载或叫做 “静态加载”利用这一点可以对代码做很多之前无法完成的优化
在开发阶段就可以做导入和导出模块相关的代码检查。结合 Webpack、Babel 等工具可以在打包阶段移除上下文中未引用的代码dead-code这种技术被称作 “tree shaking”可以极大的减小代码体积、缩短程序运行时间、提升程序性能。
后记
大家在日常开发中都在使用 CommonJS 和 ES6 module但很多人只知其然而不知其所以然甚至很多人对 AMD、CMD、IIFE 等概览还比较陌生希望通过本篇文章大家对 JS 模块化之路能够有清晰完整的认识。