从零开始的模块化开发

204 阅读4分钟

本章主要介绍前端模块化,希望能通过本章来掌握模块化开发相关知识

CommonJS

module.exports定义当前模块对外输出的对象,用require加载模块

// fs.js
let man = {
  name: 'zhangsan',
  phone: '123456',
  sex: '1'
}
module.exports = man
// CommonJS模块
let { name, phone, sex } = require('fs');

// 等同于
let _fs = require('fs')
let name = _fs.name
let phone = _fs.phone
let sex = _fs.sex

上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象_fs),然后再从这个对象上面读取 3 个变量。这种加载称为运行时加载,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”

ES6 模块

编译时加载主要又importexport两个命令组成 export可以输出变量,函数,类等

let phone = '123456'
export let name = 'zhangsan'
export {phone}
export {phone as ph}
import {name, phone, ph} from './file.js'
console.log('name', name, phone, ph)
  • export导出:export导出一个接口

    export let a = 1 // 正确
    
    let a = 1
    export {a} // 正确
    
    export 1 // 错误
    
    let a = 1
    export a // 错误
    
    

    export可以出现在模块的任意位置,但必须是顶层,不可以是块级作用域内,因为是编译时运行

  • import 加载模块

    import {name, phone as ph2, ph} from './file.js'
    console.log('name', name, ph2, ph)
    

    大括号里面的变量名,必须要和被倒入模块对外接口名称相同,如果想重新去一个变量名,可以使用as,import后面的from指定模块文件路径可以时绝对路径,也可以是相对路径,如果不带路径,要有配置文件,告诉js引擎模块位置。 import命令有变量提升的效果,会提升到头部执行,和export一样,在编译阶段执行,所以不可以放在代码块内。 使用两次或者多次import只会执行一次

  • 整体加载 除了指定加载某个输出外,可以使用一个*指定一个对象,所有输出都加载这个对象上

    import * as x from './file.js'
    console.log('xx', x.name)
    

    因为时静态分析,所以不要给他赋值(x.name = 'a')

  • 默认输出 export default 一个模块只能有一个默认输出,对应的import不需要用大括号,可以任意起名字

    // fs.js
    export default 1
    
    import num from 'fs.js'
    
  • export 和import 复合

    export { name, ph } from 'fs';
    
    // 可以简单理解为
    import { name, ph } from 'fs';
    export { name, ph };
    

    当结合成一句时,就不可以用name,ph等变量了,相当于转发了这两个接口 接口改名

    export {ph as ph2} from 'fs'
    

    接口的转发,相当于把fs中的接口全部进行了转发(除了fs中的default)

    export * from 'fs'
    
  • import()函数,支持动态加载模块

    场景:按需加载,条件加载,动态模块路径 import()加载成功后,使用.then 或者 async await 可以使用结构获取输出接口,如果有default可以直接获取

    if (true){
        import('./file.js').then((module) => {
            console.log('---', module.phone)
            console.log('---', module.default)
    
        })
    
        const a = await import('./file.js')
        console.log('aa', a.name)
    
    }
    
    

在浏览器中的使用

当浏览器遇到script标签,会停下来等到执行完毕在渲染,如果是外部标签,还会有下载的时间通过加入defer或者async来允许浏览器异步加载 defer:渲染完在执行,会按照页面出现顺序加载 async:下载完就执行,不保证按照顺序 浏览器通过设置type="module"让浏览器知道加载一个es6模块,相当于打开了defer,即渲染完在执行,按照页面出现顺序加载,如果在标签上在加上async,则变为下载完在执行,不保证顺序。

commonjs 和es6模块的异同

  1. CommonJs模块输出的是值的一个拷贝(对引用类型来说是浅拷贝),es6输出的是值的引用
  2. CommonJs模块是运行时加载,es6是编辑时输出接口
  3. CommonJs模块的require()是同步的,es6的import命令是异步的 第一个异同点:
// file.js
let a = 1
setTimeout(() => {
  a = 2
  console.log('222')
  
}, 1000);
module.exports = {
  a
}
// commonjs.js
let a = require('./file.js')
console.log('aaa',a.a)
setTimeout(() => {
  console.log('aaa2', a.a)
}, 3000);
// aaa 1
// 222
// aaa2 1

当commonjs文件加载以后,模块内部的变化影响不到输出,因为是一个原始类型会被缓存

// file2.js
export let a = 1
setTimeout(() => {
  a = 2
  console.log('---')
  
}, 1000);
// es6.js
import {a} from './file2.js'
console.log('aa',a)
setTimeout(() => {
  console.log('aaa2',a)
}, 2000);
// aa 1
// ---
// aaa2 2

es6会动态引用值,不会缓存

Node.js的模块加载

CommonJs模块是node专用,于es6不兼容,可以通过,改后缀名为.mjs或者在项目的package.js文件中type="module"两种方法,如果还用使用CommonJs模块,可以改为.cjs后缀。 总结:.mjs:以es6模块加载;.cjs:以commonjs加载;.js以package.json文件中type类型加载(默认CommonJs)