前端模块化

1,389 阅读3分钟

文献 www.manongjc.com/detail/51-l…

es6.ruanyifeng.com/#docs/modul…

www.zhihu.com/tardis/bd/a…

js加载机制(假设总是本地脚本index.js下载更快)

  • index.js
try {
    console.log(_.VERSION);
} catch (error) {
    console.log('Lodash Not Available');
}
console.log(document.body ? 'YES' : 'NO');
示例1
  • index.html
<head>
  <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.10/lodash.min.js"></script>
  <script src="./index.js"></script>
</head>

<body>
</body>
  • 控台结果
4.17.10
NO
  • 结果说明:浏览器加载脚本是采用同步模型的,都会阻塞浏览器的解析器,位于该 script 标签以上的 DOM 元素是可用的,位于其以下的 DOM 元素不可用
示例2 script上加上async属性
  • index.html
<head>
  <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.10/lodash.min.js" async></script>
  <script src="./index.js" async></script>
</head>

<body>
</body>
  • 控台结果
Lodash Not Available
YES
  • 结果说明:async不会阻塞 HTML 解析器, async 脚本每个都会在下载完成后立即执行,无关 script 标签出现的顺序
示例3 script上加上defer属性
  • index.html
<head>
  <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.10/lodash.min.js" defer></script>
  <script src="./index.js defer></script>
</head>

<body>
</body>
  • 控台结果
4.17.10
YES
  • 结果说明:defer不会阻塞 HTML 解析器, - defer 脚本会根据 script 标签顺序先后执行

deferasync的区别是:defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。

模块化之前 变量污染,请求多个script脚本性能浪费,依赖模糊

// 全局定义变量 容易变量污染
function foo(){
}
function bar(){
}
// Namespace模式: 减少全局变量数量,但还是存在污染问题,还是可以通过对象改写数据,不安全
var myUtils = {
  foo:function(){},
  bar:function(){}
}

// 匿名闭包(IIFE模式):
var module = (function () {
    var age = 9;
    var foo = function () {
        console.log('age', age);
    }
    return {
        foo,
        age
    }
})()
module.age++
console.log('module.age', module.age);// 10
module.foo()  // 9

// IIFE模式,传入依赖
var module = (function ($, window) {
    var age = 9;
    var foo = function () {
        console.log('age', age);
    }
    return {
        foo,
        age
    }
})(jQuery, window)

模块化的诞生

目的:为避免全局污染,命名冲突,按需加载,提高复用和可维护性

  • 分类:commonjs amd es6

commonjs

  • CommonJS期初主要用于Node.js环境,采用同步加载模式,如果想运行于浏览器端,需要运行其编译后的产物
  • commonjs表现形式:对外暴露 exports / module.exports,引入则用var module = require('xxx.js')
 // module1.js 1个文件可以有多个exports
 exports.foo = function(){
   console.log('module1---foo')
 }
 exports.bar = function(){
   console.log('module1---bar')
 }
 
 // module2.js 1个文件只有1个module.exports
 module.exports = {
   foo(){
      console.log('module2---foo')
   }
 }
 // 入口文件 main.js
 var module1 = require('./module1.js')
 var module2 = require('./module2.js')
 module1.foo()
 module2.foo()
 
 //index.html需要运行main.js编译后的产物才不会报错
  • CommonJS模块缓存
// lib.js
console.log('run lib.js')
module.exports =  {
    num: 1
}

//main.js
let number1 = require("./lib");

let number2 = require("./lib");
number2.num = 2

let number3 = require("./lib");
console.log(number3)
// run lib.js
// { num: 2 }

// 多次require加载number模块,但是内部只有一次打印输出;
// 第二次加载时还改变了内部变量的值,第三次加载时内部变量的值还是上一次的赋值,这就证明了后面的`require`读取的是缓存。
  • CommonJS对于基本类型的输出是深拷贝(一旦模块输出了值,模块内部的变化就影响不到这个值),对应引用类型是浅拷贝(对于复杂数据类型,由于CommonJS进行了浅拷贝,因此如果两个脚本同时引用了同一个模块,对该模块的修改会影响另一个模块)
// CommonJS 一旦输出一个值,模块内部的变化就影响不到这个值
// lib.js
var counter = 3;
var user = {
  name: "bwf",
  list: ["11", "22"],
};
function incCounter() {
  counter++;
}
function updateUser() {
  user.name = "xxxx";
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
  user: user,
  updateUser: updateUser,
};


//main.js
var mod = require("./lib");

console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3

console.log("mod.user", mod.user); // { name: 'bwf', list: [ '11', '22' ] }
mod.updateUser();
console.log("update---mod.user", mod.user); // { name: 'xxxx', list: [ '11', '22' ] }
  • CommonJS 运行时加载(但也正是由于这种动态加载,导致没有办法在编译时做静态优化。)
let num = 10;
if (num > 2) {
    var a = require("./a");
} else {
    var b = require("./b");
}

var moduleName = 'number.js'
var number = require(`./${moduleName}`)

amd(Requirejs)

  • amd 运行于浏览器端,通过异步加载模块,主要解决全局命名空间污染,相互依赖脚本加载顺序问题
  • amd表现形式:定义模块define([‘依赖项’],function(依赖对象){}),引用则通过requirejs
// module1.js (没有依赖的模块)
define(function(){
  var age = 9;
  function getAge(){
    return age;
  }
  // 暴露
  return {
    getAge
  }
})

// module2.js(有依赖的模块)
define(['module1'], function(module1){
  function showAge(){
    console.log('module2---showAge', module1.getAge)
  }
  return {
    showAge
  }
})

// main.js
requirejs.config({
  // baseUrl:'', // 基础路径
  paths:{
    module1: './module1', // key:define第一个参数的模块命名, 第二个参数是该模块对应的文件路径
    module2: './module2'
  }
})
requirejs(['module2'], function(module2){
  module2.showAge()
})

// index.html require.js是提前准备好的库
<script data-main="./main.js" src="./lib/require.js">

es6模块

  • es6模块 模块的导入是静态的,并且模块只有在被引用时才会执行,支持 Tree Shaking, 需要经过编译后运行于浏览器端
  • es6表现形式:export / export default,引入import {命名一致} from '' / import utils from ''
// 导出变量、函数和类 
export const variable = 42; 
export function add(a, b) { return a + b; } 
export class Person { /* ... */ }

// 默认导出 
export default function() { 
  console.log('This is the default export.'); 
}

// 命名导入 
import { variable, add, Person } from './module.js';
// 默认导入 
import myFunction from './module.js';

es6模块的延伸(vite开发环境运行快也是基于现代浏览器对ESM的支持)

  • 本身es6模块需要经过编译,浏览器运行编译后的产物才不会报import等语法错误,但是基于现代浏览器对## <script type=module>的支持,使得浏览器以 ES Module 的方式加载脚本
  • 默认情况下 ES 脚本是 defer 的,无论内联还是外联,- 给 script 标签显式指定 async 属性,可以覆盖默认的 defer 行为
  • 安全策略更严格,非同域脚本的加载受 CORS 策略限制

ES6 模块与 CommonJS 模块的差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。(`CommonJS会缓存结果,ES6不会)
// lib.mjs
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.mjs
import { counter, incCounter } from './lib.mjs';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

// node main.mjs
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。