一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
什么是模块?
如果你看过 webpack 官网介绍,你会发现,官网上写的有一句话提到:
webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)
那,什么是模块?听起来很深刻。模块并不是具体的事物,它是我们对同一属性的事物的称呼。
我们来看百度百科的介绍:
模块,又称构件,是能够单独命名并独立地完成一定功能的程序语句的集合(即程序代码和数据结构的集合体)
如果你觉得模块两个字很抽象,我们可以联想到生活中的一些机器。
比如豆浆机(假设只能产生生豆浆)。即,我们把豆子和水放进去,它会出来生豆浆。即 豆子 + 水 -> 豆浆机 -> 生豆浆。
比如热得快。我们把水和热得快放进去,我们就可以得到热水。即 水 + 热得快 -> 热水。
下面就到了我们的找规律环节了。热得快和豆浆机有什么共同点?
他们都有自己的特有的功能,能够对 一个东西 进行 固定的 加工 ,得到我们想要的结果。
那么这其实就是模块的定义了。那么,沿着这个思维,在代码里,具备固有功能的、支持输入(不一定非要有输入)、能够得到结果的,我们就能够把它叫做模块。
只不过,代码中的模块输入的不是水、豆子,他们输入和输出的,都是数据。比如,有个 js 文件,我使用(代码中叫引入)这个模块,我就可以展示当前的时间,那它就可以叫做一个模块。
模块可以组合。比如 现在的打豆浆的机器只能得到生豆浆,我们还需要将它煮熟,我们就将现在的豆浆机和加热模块组合,得到一个新的 自动加热的豆浆机。这个时候,它就变成了一个功能更加完善的模块。
明白了吗? 模块不分大小,模块可以组合,模块可以复用,模块是具备一定功能性事物的概称。
代码中为什么要引入模块这个概念?
编程的出现,是为了减少重复性的劳动,将相似的东西抽象出来复用,提高效率。
在一个团队开发的大项目,如果我们没有整体的划分思维,各自写各自的,需要什么干什么,会造成维护的困难、资源的浪费。
比如,豆浆机需要一个加热模块,它自己开发一个;热水器有加热模块,他也自己开发一个。那么对于各自的厂家来说,他们都需要一个匹配各自业务的加热模块工厂,对于维修人员来说,学了热水器的加热维修原理,还得学豆浆机的加热维修原理。
那如果把这个加热模块抽象形成一个单独的模块,让热水器厂家和豆浆机厂家都去适配这个模块呢?
那事情就变得简单了很多,生产、组装、维修,如果全世界都规定加热模块的参数,那么还能方便出口。实际上,现在的工业生产流程就是这样的,同一个参数标准,苹果手机的组件在各个国家生产,最后完成组装。
这就是模块化的思想,能够简化手头的工作,能够提高复用。我们提倡的前端工程化即是如此,让开发的过程接近于工业流程,形成技术的积累,完成效率的跃迁。
那么,在代码中,模块化的划分也是如此。将业务中的相似点抽象出来,形成功能模块。除了减少本项目的冗余代码,也能形成公司的模块库,长远的提高开发效率。
前端中的模块发展历史
最开始,JS 开发的目的是能够在浏览器中进行一些校验和数据处理,我们引入的方式是使用 script 标签。这样做有两大问题:命名冲突、不易于阅读。
命名冲突很好理解,谁也没办法保证两个程序员写的代码没有使用同一个变量名。毕竟,命名是编程难点之一。一旦出现这种情况,出现的错误想想就让人头皮发麻。
不易于阅读则是那么 script 标签那么长…… 引入的内容又在标签中签,一看一大堆的。
那么以上两点,指向的最终结果就是,维护困难。
一旦项目做大,效率那是直线下降。随着技术的发展,前端工程化是必然的趋势,基于这一点,JS 有了模块化的概念。
注意。我说的是 JS。 而不是前端,严格来说,是 Node.js 推进了 js 模块化的发展。
关于 nodeJS 的 commonJS 相关内容,可以参照我的文章 入门nodeJS不看这篇文章我会伤心的OK? - 掘金 (juejin.cn),里面介绍了 commonJS 的相关内容,nodeJS 的出现带动了 JS 语言模块化的发展,但是当时 commonJS 并不支持浏览器开发,即前端开发。
直到 2015 年 6 月,ES6 发布, JS 才支持模块的写法。如果你写过 react 项目,我相信,你对这种写法已经非常熟悉了。(代码参照书籍: webpack实战:入门、进阶 与 调优)
commonJS 和 ES6 Module
我简单称述一下它们的使用方法,具体内容还是可以参照我的 入门nodeJS 文章,里面写到了一些概念性的东西,以及为什么出台了这个标准后,js 就支持了这种模块化的写法。
用法展示
Common.js
// moduleA.js
module.exports = function( value ){
return value * 2;
}
复制代码
// moduleB.js
var multiplyBy2 = require('./moduleA');
var result = multiplyBy2(4);
从代码中可以看出,require 导入,module.export 导出,使用简单,路径清晰。
ES6 Module
// calculator.js
export default {
name:'calculator',
add:function(a,b){return a+b}
};
// index.js
import myCalculator from './calculator.js';
calculatro.add(2,3);
从代码中可以看出,import 导入,export 导出,使用简单,路径清晰。
webpack 与 commonJS
你可能要问了,commonJS 是 nodeJS 规范,我们说它干嘛?
如果有看过我写的文章或者或配置过 webpack,你会发现,从 npm 下载插件之后,它的引入方式是这样的:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
它使用的是 require ! 很明显,这是 nodeJS 支持的 commonJS 规范的写法。
而在项目里,我们去导入文件的时候,又是用的 import。这是为什么呢?
分两个方面来讲。这两种引入方式能够同时出现在一个前端项目中,是因为 webpack 这个工具本身默认支持多种模块标准,AMD,commonJS 以及 ES6 Module。
另一个原因,是因为我们下载相关插件的时候,我们是使用 npm。npm 是什么?是一个 NodeJS 包管理和分发工具。所以我们引入的插件,本身就是 nodeJS 写的,所以使用的是 require 引入。
看到这里,是不是感觉有些许混乱?
webpack 不是用来打包前端项目的吗?怎么会扯到 nodeJS ?
我们可以这么理解: webpack 是需要 nodeJS 环境支持的一个工具。这个工具可以使用很多 nodeJS 的工具包,这些工具包能够完成他们相应的功能,比如 less-loader 等,本质上也是一个功能模块,将less 代码转化为 css 代码。最终使用这些工具转化完成的代码,依然是浏览器可以识别的 .html,'.css','.js'。
所以 nodeJS 的发展并非和前端没有关系,他们是相互促进,相辅相成的。
区别
动态与静态
如果你看懂了上面的内容,应该对于 使用 webpack 搭建项目时,什么时候使用 require ,什么时候使用 import 应该是比较清楚的。目前来说,就以我配置的项目为例,require 只会在配置文件中用到。
那么,为什么适用于前端的 ES6 Module 用 import,适用于 nodeJS 的 commonJS 用 require? 它门有什么不同?
我们先来看 require 的引入方式。
const path = require('path');
我们可以发现,路径是放在一个括号里的,这个形式是不是有点眼熟?是不是有点像函数传参?
这意味着什么呢,我们可以动态去变更这个参数,变更这个引入地址。这就意味着,我们只有真正运行了代码,我们才能发现模块间的依赖关系。
那么 import 呢?
import myCalculator from './calculator.js';
它的依赖路径并不支持传入可变参数,这意味着,代码编译的时候,就能够知道它们的依赖关系了,不必等到运行的时候去代入变量地址。
这样做,能够减少引用层级,提高运营效率,避免引用错误,比较方便的去除没有引用的模块。
模块引用模式
使用 import 的时候,如果我们
// fun.js
export const Num = 3;
// index.js
import {Num } from './fun.js';
Num = 4;
如果我们像示例中的一样,试图修改引入的变量,浏览器的控制台会报如下错误:
这个报错意思大概是,这个变量只提供 getter ,就是只能读,不能改。
那么使用 require 呢?
// cal.js
console.log('引入')
module.exports = {num:5,}
// index.js
let A = require('./cal.js');
let B = require('./cal.js');
let C = require('./cal.js').num;
A.num = 20;
C = 8;
console.log('A', A);
console.log('B', B);
console.log('C',C);
最终运行得到的结果如下:
我们可以发现,cal.js 总共只引入了一次,而 C 的改变不影响 A 、B,A 的改变影响 B。
基于以上现象,我们可以合理推测,如果引入多次,并且不具体到变量时,多个引用指向同一个对象。如果具体到某个变量,则将该变量拷贝一份单独存储。
结语
了解了模块以后,我相信你对於 webpack 官方文档中的:
当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
这段话应该会有更深的体会了,对于模块,应该也有一个更深的理解了。
如果对你有帮助,记得给我点个赞~