前端模块化 common.js 和 export import

245 阅读6分钟

前言

现在我们开发都实现模块化开发,引入一个模块,即引入一个可以全局使用的文件,在项目编写的过程中,我们不可能把所有文件都写到一个文件里面,所以将文件拆分开来

什么是模块化

  1. 将代码或文件按一定的规则进行封装成一个模块,模块与模块之间相互独立
  2. 每一个模块内部的数据和方法都是私有的,只允许暴露出想要外部可以使用的数据才可以使用

解决了啥问题

  1. 提高了代码的复用性,更好维护
  2. 可以实现按需加载,每个模块都独立
  3. 避免了空间相互污染,命名冲突,形成隔离

常用的有

common.js

使用require()函数导入其他模块,通过设置Export对象的属性或者完全替换module.exports对象来导出公共的API

导出模块

Node定义了一个全局的exports对象{},这个对象始终会有定义,如果一个模块要导出多个值,可以直接把这些值设置为exports对象的属性

// a.js

console.log("这是a.js文件");

const sum = (a, b) => {
  return a + b;
};
const name = "你捉不到的this";


// 利用exports对象导出
exports.sumFun = sum; // 导出sum函数
exports.userName = name; // 导出name
exports.toFixedNumber = (number) => {
  return number.toFixed(2);
};

module.exports 的默认值和exports引用的是同一个对象。即 module.exports == exports

所以,在导出的时候,可以直接导出一个对象,而不是用exports属性的方式一个个导出

const sum = (a, b) => {
  return a + b;
};
const name = "你捉不到的this";

module.exports = {
	sum,    // 导出sum 函数
	userName: name  // 导出name, 并将其重命名为userName
}

导入模块

利用require导入导出的文件模块, 参数是其他要导入模块的名称,返回值是该导入模块的导出值

const FileA = require('./a');  // 可以将整个文件导入
// FileA 是一个对象,里面包含了导出的属性
FileA.sum // function
FileA.userName // '你捉不到的this'

// 也可以通过解构赋值的方式,只导入打算使用的特定属性
const { userName } = require('./a');
const { sum: sumFun } = require('./a'); // 也可在重命名

导出的是一个对象值的拷贝

一旦输出一个值,模块内部的变化就影响不到这个值了,因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成

// a.js
let name = '你捉不到的this';
let changeName = () => {name = 'lalala'}

module.exports = {
 name,
 changeName,
}

// index.js
const fileA = require("./a");
console.log(fileA.name); // '你捉不到的this'

fileA.changeNum();

// name 并没有变成lalala
console.log(fileA.name) // '你捉不到的this'

注意: 这里是一个值的浅拷贝。如果导出的是一个对象。还是会修改的

// a.js
let name = {age: '你捉不到的this'};
let changeName = () => {name.age = 'lalala'}

module.exports = {
 name,
 changeName,
}

// index.js
const fileA = require("./a");
console.log(fileA.name); // {age: '你捉不到的this'}

fileA.changeNum();

// name.age 变成lalala
console.log(fileA.name) // {age: 'lalala'}


导入的时候是同步加载

(require的内容会阻塞后续代码的执行,只有加载完成才会继续执行)

从导入的文件依次同步执行代码, 比如依次导入a.js, b.js

// a.js
console.log('a.ja')

// b.js
console.log('a.ja')


// index.js
const fileA = require("./a"); // 这里fileA是{}
console.log('1---->')
const fileB = require("./b"); // {} // 动态加载
console.log('index.js')

// 最终会先输出 a.js, 然后输出'1--->',然后输出b.js, 最后才输出index.js

运行时候加载(动态导入)

只有等代码运行的时候才能确定导入。不能进行tree shaking,所以可以在运行的代码中require

先整体加载a.js文件,加载里面的所有方法,生成一个对象_a,然后从这个对象上面去读取两个方法,只有在运行时候才能得到这个对象,所以没办法在编译的时候做静态优化

ES6模块

ES6 为JavaScript添加了import 和export 关键字

在浏览器中支持度不同,所以,es6语法会被babel 转换为common,js语法,但是因为为浏览器环境中并没有 module、 exports、 require 等环境变量。所以common.js语法在浏览器中不被允许,还需要we b pa c k进行打包,webpack 通过模拟这些变量进行打包执行

ES6导出

只需要在需要导出的声明前面加上export关键字就可以

export const name = '九思'
export const changeName = () => {}

或者只用一个export 语句申明真正要导出的值

const name = '九思'
const changeName = () => {}
const age = '18'
export {
  name,
  changeName,
	age as userAge, // 导出标识符重命名
}

注意这里的花括号实际上不会定义对象字面量,这个语法只是要求在一对花括号中给出一个逗号分隔的标识符列表

如果要导出一个值,可以使用默认导出,使用默认导出可以导出任意表达式,包括匿名函数和匿名类表达式,或者导出对象字面量

每一个模块都只能设置一个默认的导出值

const changeName = () => {}
export default changeName;

es6模块输出的是值的引用

ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。原始值变了,import 加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块


export let name = '你捉不到的this';
export let changeName = () => {name = 'lalala'}


// index.js
const {name, changeName } = require("./a");
console.log(name); // '你捉不到的this'

changeNum();

// name 变成lalala
console.log(name) //  'lalala'

ES6导入

使用import 关键字可以将导出的模块在另一个模块中访问,import的语句有两个部分,一个是要导入的标识符和标识符应当从哪个模块导入,import命令具有提升效果,会提升到整个模块的头部,首先执行。

import { name, changeName } from './a.js';

// 也可以在导入的时候重新命名
import { name as userName } from './a.js';

导入后无法在此模块定义另一个重名变量,并且也无法在import语句前使用标识符,也不能给导入的绑定重新赋值

import { name } from './a.js';
name = 'lalala'  // 抛出错误
// 如果name 是一个对象。该变去name的属性是可以的

也可以将整个模块作为一个单一的对象导入,然后模块里面的导出都可以按照对象的属性访问

import * as modelTest from './a.js';

// modelTest.name

不管import 语句中把一个模块写多少次,该模块只会执行一次,导入模块的代码执行后,实例化的模块被保存在内存中,不管引用多少次都是使用相同的模块实例

import { name } from './a.js';
import { changeName } from './a.js';

模块中即导出了默认值,也导出了其他的时候,当引用的时候,默认值必须排在非默认值前面

// a.js
export const name = 'lalala';
export default function(a, b) {
	return a + b
}

// index.js
import sum, { name } from './a.js'

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。

export { sum, name as userName } from './a.js';

不可动态导入导出

它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。让他确定哪些可以导出导入

import 函数

因import和export 语句是在编译时候,所以提出了import()函数,来支持动态加载模块,返回一个promise 对象,它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块

console.log('la la la')

if (name === 'lalala') {
  import('./a.js').then((res) => {
   console.log(res)
  }).catch((err) => {
    console.log(err)
  })
}

比如react 中用到的利用react.lazy 配合suspense, 或者路由懒加载

如有错误,还请指出,共同进步~

看到这里啦,点个赞吧~