Webpack 知识补充之模块

CommonJs

这块标准是在 2009 年提出来的,包含模块、IO、文件等。经过 Node.js 采用并做调整,所以说起 CommonJS 通常是 Node.js 中的版本了。在使用 Node.js 包管理器的年代,CommonJs 成为一颗有流量的明星了。

自身作用域

CommonJs 的模块天然有自身的作用域,所有变量和函数声明只能自己访问,外人想都别想,这个保护机制太 nice 了。

// order.js
const name = '订单系统';
// index.js
const name = '首页';
require('./order.js');
console.log(`------\n${name}`);

导出机制

模块对外暴露的方式。对于要暴露的内容可使用 module.exports来导出,其导出内容格式是一个对象。也可使用简化形式 exports

// module.exports.js
module.exports = {
  name: '订单系统',
  total: (a, b) => {
    return (a * b);
  }
}
// exports.js
exports.name = '订单系统';
exports.total = (a, b) => {
  return (a * b);
}

上面两种所要表达的功能是一样的,内在逻辑是 exports指向 module.exportsmodule.exports是初始化时建的一个空对象。所以千万不要直接给 exports赋值,还有 module.exportsexports不要并存。上面第二个文件 exports.js可这么来理解:

// 初始化(便于理解 exports 与 module.exports 的关系)
const module = {
  exports: {}
};
const exports = module.exports;

// exports.js
exports.name = '订单系统';
exports.total = (a, b) => {
  return (a, b);
}

导入机制

通过 require导入。

// 04/src/order.js
console.log('模块文件 order.js');

exports.name = 'order name';

module.exports = {
  name: '订单系统',
  total: (a, b) => {
    return a * b;
  }
};

exports.title = 'order title';
// 04/src/index.js
const title = require('./order.js').title;
const name = require('./order.js').name;

console.log(`exports.name 能展示么?-------\n${name}`);

console.log(`exports.title 能展示么?-------\n${title}`);

const total = require('./order.js').total;
setTimeout(function() {
  console.log(`module.exports 能展示么?${total(10, 10)}`);
}, 3000);

console.log('动态加载');
const modulesName = ['product.js', 'shopcar.js'];
modulesName.forEach(name => {
  require(`./${name}`).name();
});

1.缓存加载,第二次导入文件时,无需加载,因为第一次导入已经加载过了,第二次直接使用上次导入的结果; 发现没有?这是 order.js 文件这个通知在控制台里面只打印了一次,而文件 order.js实打实的引入了两次。其原理是:我们已经知道导出文件有 module这个对象,我们可能不知道的是这个对象有 loaded这么个属性(记录模块是否被夹在过),其默认值是 false,即没有加载过。当该模块第一次被加载后,loaded值会变为 true,所以第二次引入该模块就不会加载该模块了。

2.加载模块支持动态加载;

3.exportsmodule.exports不要混合使用,否则 exports会失效哦;

完整代码可查看目录 04 =>O(∩_∩)O~

ES6 Module

ES6 Module 同样是将每个文件作为一个模块,模块自身有其作用域。所不同的是,引入了关键字 import(导入)和 exports(导出),例子还是前面的例子,语法糖发生了变化。

导出机制

1.默认导出,上面例子我们都已接触过了。不过每次只能导出一个,可直接导出对象、字符串、函数等。

// 导出字符串
export default '订单系统';
// 导出对象,05/src/order.js
console.log('模块文件 order.js');

export default {
  name: '订单系统',
  total: (a, b) => {
    return a * b;
  }
};

2.命名导出,可使用关键字 as对导出变量重命名。

// 方式一,05/src/order1.js
export const name = '订单系统1';

export const total = (a, b) => {
  return a * b;
};
// 方式二,,05/src/order2.js
const name = '订单系统2';

const total = (a, b) => {
  return a * b;
};

export { name, total as getTotal };

导入机制

使用关键字 import导入,也可使用关键字 as对导入变量重命名,也可使用 as将导入变量的属性添加到后面对象(order1)中。

// 方式一
import { name, total as getProduct } from './order1';
// 方式二
import * as order2 from './order2';

示例

// 05/src/index.js
import order from './order';
import { total as getProduct } from './order1';
import * as order2 from './order2';

console.log(`order.name: ${order.name}`);
console.log(`order1 getProduct: ${getProduct(10, 10)}`);
console.log(`order2 getTotal: ${order2.getTotal(10, 10)}`);

完整代码可查看目录 05 =>O(∩_∩)O~

CommonJS 与 ES6 Module

两者本质区别在于:CommonJS 对模块依赖是“动态”的,ES6 Module 是“静态”的

1.动态,模块依赖关系是在代码运行阶段

  • require路径可动态指定;
  • 支持表达式动判断加载某个模块;

2.静态,模块依赖关系是在代码编译阶段

  • 导入、导出语句是声明式的;
  • 路径不支持表达式;
  • 导入和导出语句必须位于模块的顶层作用域(不能放在 if 语句中);

ES6 Module 优势

咋一看,CommonJS 完美 KO ES6 Module 的方式。可事实并非如此,ES6 Module 这种“静态”方式有优势:

  • 僵尸代码检测和排除,减小资源打包体积。即用静态分析工具检测模块或接口中哪些没有被调用过(比如某个组件只用了部分功能,但有可能所有代码都被加载进来了),这些加载进来未被调用的代码就是僵尸代码。静态分析可以在打包时将这些僵尸代码去掉,减小资源打包体积;
  • 编译器优化,动态模块的导入是一个对象,而 ES6 Module 可直接导入变量,减少引用层级,提高程序效率;

值拷贝和动态映射

场景:导入一个模块时,不同模块模式是不一样的。

  • CommonJS 是值拷贝,可编辑;
  • ES6 Module 是址引用,即映射,只读,即不可编辑;
// 06/src/commonJs.js
let csCount = 0;

module.exports = {
  csCount,
  csCountAdd: () => {
    csCount += 10;
  }
};
// 06/src/es6-module.js
let esCount = 0;
const esCountAdd = () => {
  esCount += 10;
};

export { esCount, esCountAdd };
// 06/src/index.js
// CommonJS Module
let csCount = require('./commonJs').csCount;
let csCountAdd = require('./commonJs').csCountAdd;

console.log(`----commonjs 初次加载----\n${csCount}`);
csCountAdd();
console.log(`----commonjs 内部自加 10 -----\n${csCount}`);
csCount += 20;
console.log(`----commonjs 启动项自加 20------\n${csCount}`);

// Es6 Module
import { esCount, esCountAdd } from './es6-module.js';
console.log(`----es6 初次加载----\n${esCount}`);
esCountAdd();
console.log(`----es6 内部自加 10 -----\n${esCount}`);
esCount += 20;
console.log(`----es6 启动项自加 20------\n${esCount}`);

通过例子及上图运行结果,可剖析

  • CommonJs 是一份值的拷贝,虽然调用了 csCount(),但是并没有造成在文件 index.js中副本(csCount)的影响;而副本(csCount)在文件 index.js中可更改;
  • ES6 是一份值的动态映射,调用了 esCount(),文件 index.js中副本(esCount)的也随之变化;而副本(esCount)在文件 index.js中是不可更改,即是只读的;

完整代码可查看目录 06 =>O(∩_∩)O~

循环依赖

通常工程中是应该尽量避免这种恶心的循环依赖的产生,因为会带来复杂度。可尽管我们知道这是不好的,也理性地避免发生。但链条长了,业务多了,人员多了,还是不知不觉中“造孽般地”写出这样的代码。

场景: A 依赖 B,B 依赖 C,C 依赖 D,D 依赖 A。其实 A 与 B 就互相依赖了。

CommonJs Module

// 07/src/a.js
const fileB = require('./b.js');
console.log(`----- a.js 文件展示 b.js 内容 ------\n`, fileB);

module.exports = '这是 a.js 文件';
// 07/src/b.js
const fileA = require('./a.js');
console.log(`----- b.js 文件展示 a.js 内容 ------\n`, fileA);

module.exports = '这是 b.js 文件';
// 07/src/index.js
require('./a.js');

我们脑海中自运行结果是

----- b.js 文件展示 a.js 内容 ------
 这是 a.js 文件
----- a.js 文件展示 b.js 内容 ------
 这是 b.js 文件

可控制台是

反复检查了代码,没错啊,可本该显示 这是 a.js 文件,为何展示 {}?不行,我还是要好好捋一捋

  • 文件 index.js导入文件 a.js,开始执行文件 a.js,第一行导入文件 b.js,此时进入文件 b.js内部;
  • 文件 b.js第一行又导入文件 a.js,循环依赖由此产生。这时执行权并没有交回给文件 a.js,而是直接取导出值,此刻文件 a.js还未执行技结束,导出值就默认为空对象,因此文件 b.js执行打印语句时,显示 ----- b.js 文件展示 a.js 内容 ------ {};
  • 文件 b.js执行结束了,执行权接着交回给文件 a.js, 文件 a.js继续执行第二行,然后在控制台打印 ----- a.js 文件展示 b.js 内容 ------ 这是 b.js 文件。至此,这个流程结束;

完整代码可查看目录 07 =>O(∩_∩)O~

ES6 Module

// 08/src/a.js
import fileB from './b.js';
console.log(`----- a.js 文件展示 b.js 内容 ------\n`, fileB);

export default '这是 a.js 文件';
// 08/src/b.js
import fileA from './a.js';
console.log(`----- b.js 文件展示 a.js 内容 ------\n`, fileA);

export default '这是 b.js 文件';
// 07/src/index.js
import a from './a.js';

我们脑海中自运行结果自然也是

----- b.js 文件展示 a.js 内容 ------
 这是 a.js 文件
----- a.js 文件展示 b.js 内容 ------
 这是 b.js 文件

可控制台是

完整代码可查看目录 08 =>O(∩_∩)O~

我晕,还是不对,这次不是空对象了,却是 undefined。那循环依赖该如何解决呢?

---> 等我下篇 <---

上一篇:webpack 简介

原文链接:juejin.im

上一篇:TypeScript 3.8 Beta
下一篇:JavaScript 对象赋值和浅拷贝的区别

相关推荐

  • 🚀webpack 4 beta — try it today!🚀

    Now that webpack is a 0CJS (Zero Configuration) outofthebox bundler, we will lay groundwork in 4.x a...

    2 年前
  • 🔥手写大厂前端知识点源码系列(上)

    如今前端攻城狮的要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。

    6 天前
  • 🔥一步一步的带你走进Webpack4的世界

    前言 webpack是当下最热门的前端资源模块化管理和打包工具,它可以将许多松散的模块按照依赖和规则打包成符号生产环境部署的前端资源,还可以将按需加载的模块进行代码分割。

    1 个月前
  • 🔥Webpack 插件开发如此简单!

    本文使用的WebpackQuicklyStarter快速搭建 Webpack4 本地学习环境。 建议多阅读 Webpack 文档《Writing a Plugin》章节,学习开发简单插件。

    1 个月前
  • 🔥 从最近流行的一幅 CSS 风景画中学习 2 个知识点

    最近 CodePen 流行的一张用纯 CSS 绘制的风景画令人印象深刻: (/public/upload/d250206ad61c150f0fbec8efcd474c2a) 天空的眩光,睡眠反...

    9 天前
  • 🔥 2020年从基础到进阶,测试你有多了解 JavaScript,刷新你的知识!🚀

    【译】JavaScript 进阶问题列表 从基础到进阶,测试你有多了解 JavaScript,刷新你的知识 答案在问题下方的折叠部分,点击即可展开问题。 1. 输出是什么? A: Lydi...

    3 个月前
  • (独家!)webpack 5 changelog 全文翻译

    ★ webpack 团队于北京时间 10 月 12 日凌晨发布了 版本,本文译自 。此部分主要面向非插件开发的 webpack 使用者。 ” 简要说明 此版本重点关注以下内容: ...

    6 个月前
  • 高级前端知识点汇总

    高级前端知识点汇总 1.首屏加载优化 2.搜索引擎优化(SEO) 3.请求优化 4.Vue服务器端渲染 (SSR) 5.W3C标准 6.vue render优化...

    2 个月前
  • 预告:JavaScript模块全览

    之前写的文章急速Js全栈教程(https://segmentfault.com/a/1190000016101940)得到了不错的阅读量,霸屏掘金头条3天,点赞过千,阅读近万,甚至还有人在评论区打广告...

    2 年前
  • 面试知识点之javascript中变量与函数重名规则

    声明 相信大家都知道变量提升,函数提升,可重名的时候又是如何处理? 试问一下2个场景的输出值分别多多少? 情景一 情景二 答案是两个场景输入都是一样的.结果都为: ...

    2 个月前

官方社区

扫码加入 JavaScript 社区