写在前面
最近一两个月在思考未来自己的职业规划,团队内小伙伴陆陆续续走的走,来的来。大家都处于职业的焦虑之中,我一直都很想进一家大厂,目前仍然没有机会,也没有能力能够进去。毕业已经3年整了,不能继续说打好基础,把所有的业余精力都放在js基础学习上。应该着眼于自己的优势和有竞争力的方面努力,这样才不容易被取代。所以前端工程化是必不可少的深挖领域,以后博客都会产出工程化相关的文章。
前端工程化是一个很大的议题,基本上工程化体系是离不开webpack和js模块。首先先从模块说起,面试的时候免不了会被问到几个模块相关的问题。例如
- 说一说模块化的好处?前端模块化解决了哪些问题?
- 说一说es module和commonjs module的区别?commonjs require函数的工作原理?
- module.export、export 与 export defalut 有什么区别?
...
...
...
js模块介绍
模块化就是将一个大文件拆分成相互依赖的小文件,再通过打包工具进行统一的拼装和加载。有人会问了模块化的好处是什么,或者说为什么要做模块化?其实这样做的好处可以从两个角度分析
- 前端开发人员可以遵循统一标准规范,多人协作同时进行系统开发,加快开发速度
- 代码稳定,模块间相互隔离,相互独立,互不影响。解决命名冲突等。
- 代码可读性:代码被分割成细小的整体,可读性变强。
- 可复用性:模块间相互独立,互不影响,可复用性高。
- 可维护性:依赖关系明确清晰,易于维护。
js的模块标准有两种 commonjs module和es6之后开始支持的es module。node应用采用的commonjs module规范,web应用一般采用es module规范。
commonjs module和es module
commonjs module简称cjs,es module简称mjs,下同。
示例源码
cjs
- require函数,用于导入
- module.exports或者exports变量,用于导出
例如
//add.js
function add(a,b){
return a+b;
}
module.exports={add};
//index.js
const {add} =require('./add');
add(1,2);
模块的导入导出需要注意的问题
- exports不能直接对其赋值,例如下代码是不会正常导出的
//add.js
function add (a,b){
return a+b
};
exports={add};
- 在同一个文件中,exports和module.exports尽量不要同时使用,例如下代码
exports.add=function(a,b){
return a+b;
}
module.exports={
name:'jack'
}
- module.exports或者exports尽量放在文件的末尾,可以提高代码的可读性
- require函数是CommonJS规范之中,用来加载其他模块的函数。它其实不是一个全局函数,而是指向当前模块的module.require函数,而后者又调用Node的内部命令Module._load。
- require函数有缓存,当第一次加载某个模块时,node会缓存该模块,以后再加载该模块时,就直接从缓存中取出该模块的module.exports属性
- require函数的加载规则,根据参数的不同格式,require命令去不同路径寻找模块文件
- 如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。
- 如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件
- 如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)
- 如果参数字符串不以“./“或”/“开头,而且是一个路径,比如require('example-module/path/to/file'),则将先找到example-module的位置,然后再以它为参数,找到后续路径
- 如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析
- 如果想得到require命令加载的确切文件名,使用require.resolve()方法
mjs
- import命令用于导入
- export(命名导出)或者export default(默认导出)命令用于导出
例如
//add.js
function add(a,b){
return a+b;
}
export {
add
}
// index.js
import {add} from './add';
add(1,2);
到这里可以思考一个问题,
我们可以直接export default一个字面量对象吗?在导入的时候能解构到想要到值吗?来看下下面的代码
//add.js
function add(a,b){
return a+b
}
export default {
add
}
// index.js
import {add} from "./add.mjs";
add(1,2);
上面的代码其实是会报错的,报错的文件是index.js,export default {...}这种写法是没有不会报错的,只是不规范。那为啥index.js中解构的形式导出会报错呢,我们先来看看报的什么错误
import { add } from './add.mjs';
^^^
SyntaxError: The requested module './add.mjs' does not provide an export named 'add'
看报错信息说没有请求的模块中export没有提供add。既然这样,我们就直接给请求到的add模块赋一个值,然后我们打印出来看看究竟是一个什么东西,add模块的代码不变,index.js修改后的代码如下
// index.js
import util from "./add.mjs";
cosnole.log(util);
运行之后我们可以看到控制台输出
> [lzm]% node index.mjs
> { add: [Function: add] }
我们可以看到是一个拥有add函数的对象,按理说应该可以结构赋值才对呀。其实从上面的报错能看出来,import的解构会自动匹配export,如果没有则会报错。
可以再思考一个问题,在node环境下可以使用es module吗?
答案是可以的,以上例子都是在node环境下运行的。现在node已经支持es module规范了,参考官网的解释
esm_enabling
commonjs module和es module的异同
最本质的区别是模块依赖解析的时机不同
运行时和编译时
所谓运行时即代码已经解析成机器可以识别的形态,代码可以直接运行的阶段。而编译时即我们的js代码解析成机器可以识别的形态的过程。
- cjs在运行时解析模块依赖,mjs在编译时解析模块依赖
cjs的加载机制是输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
cjs是在运行时才确定引入, 然后执行这个模块, 相当于是调用一个函数, 返回一个对象.
mjs是语言层面的, 导入导出是声明式的代码集合. 声明式的意思就是说, 直接利用关键字声明说我要导入/导出一个模块啦, 而不是粗鄙(节目效果)地将一个对象赋值给一个变量
如何处理模块的循环加载?
CommonJS模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行,CommonJS的做法是,一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。require函数第一次导入时会自动缓存结果,第二次再导入模块时会直接给到结果。
import不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值
es 模块并没有帮我们解决循环依赖,所以循环依赖问题需要开发者自己解决。那么问题又来了,在node环境下我们在使用es 模块的时候是不是会出现死循环呢?我们先来看个例子
//add.mjs
import { log } from './log.mjs';
function add(a, b) {
log(a, b)
return a + b;
}
export {
add
}
//log.mjs
import { add } from './add.mjs';
add(1, 2);
function log() {
console.log(...arguments);
};
export {
log
}
在node环境下运行
node add.mjs
我们会发现控制台不会无限打印,只打印出一次1 2。
其他模块
除了commonjs module和es module模块,还有amd,cmd和umd等等。amd示例实现依赖于库requirejs,cmd示例实现依赖于库seajs
示例源码