当前位置: 首页 > news >正文

网站根目录多文件互联网论坛

网站根目录多文件,互联网论坛,无备案网站广告如何做,党建专栏 文字说明 网站建设前言 JavaScript 语言诞生至今,模块规范化之路曲曲折折。 前言 JavaScript 语言诞生至今,模块规范化之路曲曲折折。社区先后出现了各种解决方案,包括 AMD、CMD、CommonJS 等,而后 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 lang="en"><head><meta charset="UTF-8"><meta ><title>【深度全面】JS模块规范进化论</title>
    </head><body><!-- 所有 script 标签必须保证顺序正确,否则会依赖报错 --><script src="./js/config.js"></script><script src="./js/utils.js"></script><script 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): any

RequireJS

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  -->
    <!-- ...省略其他 -->
    <body><script 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.exportsexports放到 “明面” 上来写,可能就更清楚了:

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

模块初始化时,exportsmodule.exports指向同一块内存,exports被重新赋值后,就切断了跟原内存地址的关系。

所以,exports要这样使用:

// a.js
exports.s = 'i am ronffy';// b.js
var a = require('./a.js');
console.log(a.s); // i am ronffy

CommonJS 和 CommonJS2 经常被混淆概念,一般大家经常提到的 CommonJS 其实是指 CommonJS2,本文也是如此,不过不管怎样,大家知晓它们的区别和如何应用就好。

CommonJS 与 AMD

CommonJS 和 AMD 都是运行时加载,换言之:都是在运行时确定模块之间的依赖关系。

二者有何不同点:

  1. CommonJS 是服务器端模块规范,AMD 是浏览器端模块规范。
  2. CommonJS 加载模块是同步的,即执行var a = require('./a.js');时,在 a.js 文件加载完成后,才执行后面的代码。AMD 加载模块是异步的,所有依赖加载完成后以回调函数的形式执行代码。
  3. [如下代码]fschalk都是模块,不同的是,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 只是写法不同,结果是一样的,都是把prefixapi分别导出。

  • 方式 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 在处理以上几种导入模块接口的方式时都是编译时处理,所以importexport命令只能用在模块的顶层,以下方式都会报错:

// 报错
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,打印结果如下:

  1. o init
    模块即使被其他多个模块导入,也只会加载一次,并且在代码运行完成后将接口赋值到module.exports属性上。
  2. a o.num: 0
    模块在加载完成后,模块内部的变量变化不会反应到模块的module.exports
  3. b o.num: 2
    对导入模块的直接修改会反应到该模块的module.exports
  4. 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,打印结果如下:

  1. o init
    模块即使被其他多个模块导入,也只会加载一次。
  2. o.num: 0
  3. o.num: 1
    编译时确定模块依赖的 ES6 module,通过import导入的接口只是值的引用,所以num才会有两次不同打印结果。
  4. o.getNum: 1

对于打印结果 3,知晓其结果,在项目中注意这一点就好。这块会涉及到 “Module Records(模块记录)”、“module instance(模快实例)” “linking(链接)” 等诸多概念和原理

ES6 module 是编译时加载(或叫做 “静态加载”),利用这一点,可以对代码做很多之前无法完成的优化:

  1. 在开发阶段就可以做导入和导出模块相关的代码检查。
  2. 结合 Webpack、Babel 等工具可以在打包阶段移除上下文中未引用的代码(dead-code),这种技术被称作 “tree shaking”,可以极大的减小代码体积、缩短程序运行时间、提升程序性能。

后记

大家在日常开发中都在使用 CommonJS 和 ES6 module,但很多人只知其然而不知其所以然,甚至很多人对 AMD、CMD、IIFE 等概览还比较陌生,希望通过本篇文章,大家对 JS 模块化之路能够有清晰完整的认识。

http://www.zhongyajixie.com/news/24603.html

相关文章:

  • 网站面包屑导航怎么做的国家卫生健康委
  • 网站开发原型网站域名解析ip
  • h5能做网站开发吗百度收录时间
  • 哪里可以做网站系统免费创建个人网站
  • wordpress 博客 简书百度seo和sem
  • 西安网站开发公司排行榜爱站网关键词挖掘查询
  • 北京网站建设方案报价百度seo推广计划类型包含
  • 企业门户网站开发品牌运营推广方案
  • 网站代做如何做好网上销售
  • 婚纱摄影的网站怎么做推广引流app
  • 沧州网站优化河南网站推广那家好
  • 仪征网站建设域名地址查询
  • 网站改版 升级的目的排名优化关键词
  • 广州黄埔做网站html友情链接代码
  • 徐州城乡建设网站厦门人才网官网登录
  • 电商美工培训哪个学校好seo网站关键词快速排名
  • ic外贸网站建设手机优化什么意思
  • 沧州做网站价格公司做网站怎么做
  • 滨州网站建设制作搜索引擎网络排名
  • 黑龙江网站建设seo 是什么
  • 购物网站支付功能怎么做东莞做一个企业网站
  • 网站服务器租用一年多少钱啊石家庄新闻头条新闻最新今天
  • 网站链接网址怎么做宁波seo优化公司排名
  • 外贸网站产品关键词培训班招生方案
  • 餐饮网站建设方案国外免费ip地址
  • 坪山商城网站建设哪家公司靠谱培训网站排名
  • b2b采购平台是什么意思网站内部链接优化方法
  • 万户做网站怎么样东莞快速优化排名
  • 西安网站建设排行榜seo主要做什么工作内容
  • wordpress 判断文章页网站搜索排名优化