模块化历程
-
前端开发为什么需要模块化?
- 最初的时候前端工作更多的是页面内容的制作,往往只是很简单的网页结构搭建,或 css 样式编写,再难点就是 UI 交互,前后端数据交互,因此一个页面的开发工作量不会太大,所以也不会依赖太多的外部文件,其中的逻辑代码也不会有很多;
- 随着 Web 技术的发展,前端项目也越来越大,移动端的需求也越来越多,所以现在需要完成的都是组件化的前端开发;
- 在这种情况下,之前传统的前端开发模式中的一些问题也就逐渐的凸显出来了;
-
传统开发常见问题
- 命名冲突和污染;
- 代码冗余,无效请求过多,影响加载速度;
- 文件间的依赖关系复杂,容易出错;
-
什么是模块?
- 模块可以理解为,大篇幅代码被一种程序化的结构和组织方式拆分之后而生成的小而精,并且具有低耦合特点的松散片段;
- 模块化开发更像是对这些片段进行组合使用,从而完成项目整体业务逻辑;
- 这样的项目也就更加容易维护和管理了;
-
总结
- 模块化是前端走向工程化中的重要一环;
- 早期 JavaScript 语言层面没有模块化规范,开发者利用函数、对象或自执行函数实现代码的分块管理,后来由个人或社区推动,产出了 CommonJS、AMD、CMD 这些模块化规范;
- ES6 中将模块化纳入标准语言规范中;
- 当下常用规范是 UMD(统一模块规范)、CommonJS 和 ES Module ,前者用于 Node 平台下的开发,后者用于浏览器平台下的开发;
ESModule 规范
ES6 在语言标准的层面上,实现了模块功能,成为浏览器和服务器通用的模块解决方案,完全可以取代 CommonJS 和 AMD 规范,基本特点如下:
- 每一个模块只加载一次,每一个 JS 只执行一次,如果下次再去加载同目录下同文件,直接从内存中读取;
- 每一个模块内声明的变量都是局部变量,不会污染全局作用域;
- 模块内部的变量或者函数可以通过 export 导出;
- 一个模块可以导入别的模块;
模块功能主要由两个命令构成:
- export 命令用于规定模块的对外接口;
- import 命令用于输入其他模块提供的功能;
export 导出
// 第一种:分条导出
export var name = 'foo module'
export function hello() {
console.log('hello world')
}
export class Person {}
// 对应的导入 => import { name, hello, Person } from './a.js';
// 第二种:集中导出 =>
var name = 'foo module';
function hello() {
console.log('hello world');
}
class Person { }
export { name, hello, Person }
// 对应的导入 => import { name, hello, Person } from './a.js';
// 第三种:导出重命名
export {
name as default, // 作为默认项导出,导入需要重命名,import {default as fooName} from './a.js'
hello as fooHello,
Person as FooPerson
}
// 对应的导入 => import { default as fooName, fooHello, FooPerson } from './a.js';
// 第四种:默认导出参数
export default name
// 对应的导入 =>
// import abc from './a.js'; 导入默认参数可随意命名(除关键字外)
// 等价于
// import { default as fooName } from './a.js';
export default {
name,
hello,
Person
}
// 对应的导入 => import abc from './a.js'; abc 是字面量对象 { name, hello, Person }
// 不能 import { name, hello, Person } from './a.js' 导出,错误示例
import 导入
// 第一种:import...from... 必须在文件的最顶层,最外层的作用域;
// 可以是相对路径、绝对路径、完整的url(可以引用 cdn 上的文件, 但不能以字母开头,js会以为是加载第三方插件)
import { name } from './module.js'
import { name } from '/user/export/module.js'
import { name } from 'http://localhost:3000/2-2/export/module.js'
// 第二种:只执行某个模块,不提取,多用于加载其他项目中的子模块
import {} from './a.js'
import './a.js' // 简写
// 第三种:动态导入(可以放在任意文件中的任意位置)
import('./a.js').then(function(module){
console.log(module)
})
// 第四种:导入默认参数
import { name, age, default as title} from './a.js'
import title, { name, age } from './a.js' // 简写
// 第五种:导入,提取所有成员
import * as mod from './a.js'
console.log(mod)
// 第六种:导入单个默认参数并重命名
import default as title from './module.js'
注意
export 后面跟的 { } 是固定用法,不是字面量对象;
export default { name, age } 中 { } 是字面量对象;
import 后面跟的 { } 是固定用法,不是字面量对象,与 export 是对应的引用关系,是存储空间地址的引用;
import 导入的成员是一个只读对象,不可在导入文件中修改;
CommonJS 规范
CommonJS 是语言层面上的规范,类似于 ECMAScript ,而模块化只是这个规范中的一部分;
CommonJS 语法是同步的,可以将 require 写到条件中动态导入;
为模块包装提供的全局对象:
- module、exports:处理模块的导出;
- required:实现模块的加载;
- __filename:返回正在执行脚本文件的绝对路径;
- __dirname:返回正在执行脚本的所在目录;
module 属性
任意一个 js 文件就是一个模块,具有独立作用域,可以直接使用 module,它表示主模块(入口文件),module 本身还拥有很多有用的属性:
require 导入
导入模块可以使用 绝对路径、相对路径、模块名,最终都会转换成 绝对路径;
// 导入
let c1 = require("./b");
let c2 = require("./c");
console.log(c1); // { c:3, d:4 }
console.log(c2); // { x:1, y:2 }
exports 导出
// 导出多个变量
exports.c = 3;
exports.d = 4;
// 整体导出
let a = {
x: 1,
y: 2,
};
// 整体导出为顶级导出,会覆盖单独导出
module.export = a;
module.exports VS exports
在 CommonJS 规范中只规定了通过 module.exports 执行模块的导出数据操作,而单个 exports 实际上是 Nodejs 自己为了方便操作,提供给每个模块的变量,它实际上指向了 module.exports 指向的内存地址;
因此可以直接通过 exports 导出相应的内容,不能直接直接给 exports 重新赋值,这等于切断了 exports 和 module.exports 之间的联系;
CommonJS VS ESModule
导出、导入方式不同:CommonJs 使用的是 module.exports 和 require;ESModule 使用的是 export 和 import;
JS 在加载时分为两个阶段:编译和执行
- CommonJs 是动态的依赖,同步执行;
- ESModule 既支持动态,也支持静态,动态依赖是异步执行的;
模块的引用类型不同:CommonJs 对基本类型传递值,ESModule 对基本类型是传递引用;
CommonJs 的 this 是当前模块,ESModule 的 this 是 undefined;
对 webpack 来说,想要支持 tree shaking,包必须采用 ESModule 规范;
面试题
下面的代码输出什么结果?
// module counter
var count = 1;
export {count}
export function increase(){
count++;
}
// module main
import { count, increase } from './counter'; // 引入的和导出的变量是一个地址
import * as counter from './counter'; // 引入的和导出的变量是一个地址
const { count: c } = counter; // counter 被解构了,c 是新的内存地址
increase();
console.log(count); // 2
console.log(counter.count); // 2
console.log(c); // 1
node👉 全局对象
上一篇