前端模块化详解

模块化的理解

什么是模块化

  • 将一个复杂的程序,按一定的规则(规范)封装成几个块(文件),并进行组合在一起
  • 块的内部数据与实现是私有的,只是向外暴露一些接口(方法)与外部其它模块通信

模块化有什么好处?(为什么要使用模块化)

  • 避免命名冲突(减少命名空间污染)

  • 更好的分离,按需加载

  • 更高复用性,高可维护性

  • 解决引入多个<script>后容易出现的问题

  • 请求过多:依赖多个模块,那样就会发送多个请求,导致请求过多

    • 依赖模糊:不知道依赖之间的具体依赖关系是什么,容易导致加载先后顺序出错。
    • 难以维护:以上两种原因必然导致难以维护

模块化发展史

早期 JavaScript 开发很容易存在全局污染依赖管理混乱问题,这些问题在多人开发前端应用的情况下变得更加棘手。举个?:

<body>
  <script src="./index.js"></script>
  <script src="./home.js"></script>
  <script src="./list.js"></script>
</body>

没有模块化,那么 script 内部的变量是可以相互污染的。比如上述代码中 ./index.js 文件和 ./list.js 文件为小 A 开发的,./home.js 为小 B 开发的。index.js中定义了变量name是一个string,home.js中定义的name变量是一个function,name变量这时候就被互相污染了

在规范出现前的解决方案

1. 全局function模式 : 将不同的功能封装成不同的全局函数
function m1(){
  //...
}
function m2(){
  //...
}

问题: 污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系

2. namespace模式 : 简单对象封装

作用: 减少了全局变量,解决命名冲突

问题: 数据不安全(暴露所有模块成员,外部可以直接修改模块内部的数据)

let myModule = {
  data: 'www.baidu.com',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }

}
myModule.data = 'other data' //能直接修改模块内部的数据
myModule.foo() // foo() other data
3. IIFE模式:匿名函数自调用(闭包)

作用: 数据是私有的, 外部只能通过暴露的方法操作

编码: 将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口

问题: 如果当前这个模块依赖另一个模块怎么办?

// module.js文件

(function(window) {
  let data = 'www.baidu.com'

  //操作数据的函数

  function foo() {

    console.log(`foo() ${data}`)

  }

  function bar() {
    console.log(`bar() ${data}`)
    otherFun() //内部调用
  }
  function otherFun() {
    //内部私有的函数
    console.log('otherFun()')
  }
  //暴露行为
  window.myModule = { foo, bar }
})(window)



// index.html文件
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
    myModule.foo()
    myModule.bar()
    console.log(myModule.data) //undefined 不能访问模块内部数据
    myModule.data = 'xxxx' //无法修改的模块内部的data
    myModule.foo() //没有改变
</script>
4. **IIFE模式增强 : 引入依赖 —–**这也是现代模块实现的基石
// module.js文件

(function(window, $) {
  let data = 'www.baidu.com'

  //操作数据的函数

  function foo() {

    console.log(`foo() ${data}`)

    $('body').css('background', 'red')
  }

  function bar() {
    console.log(`bar() ${data}`)
    otherFun() //内部调用
  }
  function otherFun() {
    //内部私有的函数
    console.log('otherFun()')
  }
  //暴露行为
  window.myModule = { foo, bar }
})(window, jQuery)



 // index.html文件
  <!-- 引入的js必须有一定顺序 -->
  <script type="text/javascript" src="jquery-1.10.1.js"></script>
  <script type="text/javascript" src="module.js"></script>
  <script type="text/javascript">
    myModule.foo()
  </script>

上面的例子需要在myModule.js中使用jQuery,所以必须先引入jQuery库,就把这个库当作参数传入,。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。

模块化规范

在ES6出来之前:

  • CommonJs–就是require这种,node里面使用的,是node的模块规范。
  • AMD–是Require.js在推广的过程中对模块定义的规范化产出。
  • CMD–是淘宝Sea.js在推广的过程中对模块定义的规范化产出

ES6:

  • ES6Module–就是import、export这类的,我们对这个应该很熟。

CommonJS

每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。( Node 是 CommonJS在服务器端一个具有代表性的实现。

特点

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。(也就是说,输入的是被输出的值的拷贝。
  • 模块加载的顺序,按照其在代码中出现的顺序。

具体使用

暴露模块:
module.exports = value 或 exports.xxx = value

引入模块:
require(module)

如果是第三方模块,module为模块名;如果是自定义模块,module为模块文件路径

// 举个?:example.js
const x = 5;

const add = function (a) {

  return a + x;

};

// 暴露变量
module.exports.x = x;
module.exports.add = add;



// 引入变量
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.add(1)); // 6
CommonJS暴露与引入的是什么?

暴露: CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性

引入(加载): require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。

CommonJS加载原理

CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

{

    id: 'xxx',
    exports: { ... },
    loaded: true,
    ...
}

上面代码就是 Node 内部加载模块后生成的一个对象。该对象的id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。

以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。

也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。

AMD

概念与特点

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。

AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。

但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要来的早。

优点:AMD模块定义的方法非常清晰,不会污染全局环境,能够清楚地显示依赖关系

基本用法

AMD是”Asynchronous Module Definition”的缩写,即“异步模块定义”。

AMD规范很简单,只有一个API,即define函数:

define([module-name?], [array-of-dependencies?], [module-factory-or-object])

其中:

  • module-name: 模块标识,可以省略。
  • array-of-dependencies: 所依赖的模块,可以省略。
  • module-factory-or-object: 模块的实现,或者一个JavaScript对象。
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:

require([module], callback);

  • 第一个参数[module],是一个数组,里面的成员就是要加载的模块;
  • 第二个参数callback,则是加载成功之后的回调函数。
//定义没有依赖的模块

define(function(){
   return 模块
})


//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
   return 模块
})

// 引入使用模块
require(['module1', 'module2'], function(m1, m2){
   使用m1/m2
})

CMD

CMD(Common Module Definition)是国内大牛玉伯在开发SeaJS的时候提出来的,属于CommonJS的一种规范规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。

CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。

基本用法

//定义没有依赖的模块

define(function(require, exports, module){
  exports.xxx = value
  module.exports = value
})




//定义有依赖的模块
define(function(require, exports, module){
  //引入依赖模块(同步)
  var module2 = require('./module2')
  //引入依赖模块(异步)
    require.async('./module3', function (m3) {
    })
  //暴露模块
  exports.xxx = value
})

// 引入使用模块
define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

UMD (Universal Module Definition)

UMD 是 JavaScript 模块的通用模块定义模式。这些模块能够在任何地方工作,无论是在客户端、服务器还是其他地方。

// 通常用来加载不同的规范
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery', 'underscore'], factory);
    } else if (typeof exports === 'object') {
        // Node, CommonJS之类的
        module.exports = factory(require('jquery'), require('underscore'));
    } else {
        // 浏览器全局变量(root 即 window)
        root.returnExports = factory(root.jQuery, root._);
    }
}(this, function ($, _) {
    // 方法
    function a(){}; // 私有方法,因为它没被返回 (见下面)
    function b(){}; // 公共方法,因为被返回了
    function c(){}; // 公共方法,因为被返回了
    // 暴露公共方法
    return {
        b: b,
        c: c
    }
}));

ES Module(ES6 模块化)

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

特点

1、静态语法: ES6 module 的引入和导出是静态的,import 会自动提升到代码的顶层 ,import , export 不能放在块级作用域或条件语句中。

2、执行特性: ES6 模块提前加载并执行模块文件,ES6 模块在预处理阶段分析模块依赖,在执行阶段执行模块,两个阶段都采用深度优先遍历,执行顺序是子 -> 父。

3、导出绑定: 不能修改import导入的属性,直接修改会报错

注:如果导出的是对象,是可以成功改写他的属性的,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。

基本用法

import、export

import会自动提升到代码的顶层,并且import、export 只能出现在代码的顶层
export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。

为什么?

引擎处理import语句是在编译时,这时不会去分析或执行if语句,所以import语句放在if代码块之中毫无意义,因此会报句法错误,而不是执行时错误。

所以,importexport命令只能在模块的顶层,不能在代码块之中(比如,在if代码块之中,或在函数之中)。

这样设计的不足?

这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。所以import命令无法取代 Node 的require方法,因为require是运行时加载模块,import命令无法取代require的动态加载功能。

const path = './' + fileName;
const myModual = require(path);

上面的语句就是动态加载,require到底加载哪一个模块,只有运行时才知道。import命令做不到这一点。

所以 ===>  ES2020提案 引入import()函数,支持动态加载模块。

导入、导出 示例
// 导出info.js   export && export default
const name = "张三";

export const age = "18";
export default name;



// 导入
import name, { age } from "./info";



console.log(name, "name");
console.log(age, "age");

// 执行 module 不导出值 多次调用 module 只运行一次。
import 'module' 


// 用星号( * )指定一个对象,所有输出值都加载在这个对象上面
import * as circle from './circle';

// 导出,这样导出的模块 import时可以指定任意的名字
export default {xxx}
export 与 import 的复合写法

注:写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

// 先输入后输出同一个模块
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };

模块的接口改名和整体输出

// 接口改名
export { foo as myFoo } from 'my_module';
// 整体输出
export * from 'my_module';

默认接口

export { default } from 'foo';


// 具名接口改为默认接口
export { es6 as default } from './someModule';


// 等同于
import { es6 } from './someModule';
export default es6;



// 默认接口改具名接口
export { default as es6 } from './someModule';

import * as someIdentifier from "someModule";对应的符合写法:

export * as ns from "mod";


// 等同于
import * as ns from "mod";
export {ns};

import() :

import()与import的参数一致,并且可以动态使用,加载模块。返回一个 Promise 对象(可以用在async函数中), 返回的 Promise 的 then 成功回调中,可以获取模块的加载成功信息。

import()应用
  • 按需加载
button.addEventListener('click', event => {
    import('./dialogBox.js')
.then(dialogBox => {
    dialogBox.open();
})

.catch(error => {
    /* Error handling *
})
});
  • 条件加载
if (condition) {
    import('moduleA').then(...);
} else {
    import('moduleB').then(...);
}
  • 动态的模块路径
import(f())
.then(...);

注:import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。

作用

  • webpack借助 ES Module 的静态导入导出的优势,实现了 tree shaking。
  • ES Module 还可以 import() 懒加载方式实现代码分割。
  • 引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

除了静态加载带来的各种好处,ES6 模块还有以下好处。

  • 不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
  • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
  • 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。

浏览器 & Node.js 加载ES6模块

浏览器

默认

浏览器对于带有 type="module"<script> ,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。并且如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。

<script type="module" src="./foo.js"></script>

async

<script>标签的async属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。

<script type="module" src="./foo.js" async></script>

一旦使用了async属性,<script type="module">就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。

一些注意点

  • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
  • 模块脚本自动采用严格模式,不管有没有声明use strict
  • 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。
  • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。
  • 同一个模块如果加载多次,将只执行一次。

利用顶层的this === undefined这个语法点,可以侦测当前代码是否在 ES6 模块之中。示例:

import utils from 'https://example.com/js/utils.js';
const x = 1;
console.log(x === window.x); //false
console.log(this === undefined); // true

Nodejs

Node.js 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。从 v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。

在node中

.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置。type为module时可以加载ES6模块

注意,ES6 模块与 CommonJS 模块尽量不要混用。require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import

举个?,package.json文件中:

以下代码会被解析为ES6模块,但如果去掉type,默认解释为CommonJS模块

{

    "type": "module",
    "main": "./src/index.js"
}

循环加载

CommonJS 中的循环加载

CommonJS 模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。

// 举个?,a.js
exports.done = false;
var b = require('./b.js');

console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');


// b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');

如果先执行a.js,先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。b.js 执行到第二行,就会去加载a.js,这时,就发生了“循环加载”。系统会去a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,从exports属性只能取回已经执行的部分,也就是exports.done = false,然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。于是,a.js接着往下执行,直到执行完毕。

// 执行脚本
var a = require('./a.js');
var b = require('./b.js');

console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
// 运行结果
在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true

上面的代码证明了两件事。一是,在b.js之中,a.js没有执行完毕,只执行了第一行。二是,main.js执行到第二行时,不会再次执行b.js,而是输出缓存的b.js的执行结果,即它的第四行。

ES6 module 中的循环加载

ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

// a.mjs

import {bar} from './b';

console.log('a.mjs');

console.log(bar);
export let foo = 'foo';



// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';

上面代码中,a.mjs加载b.mjsb.mjs又加载a.mjs,构成循环加载。执行a.mjs,结果如下。

$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined
执行过程

首先,执行a.mjs以后,引擎发现它加载了b.mjs,因此会优先执行b.mjs,然后再执行a.mjs。接着,执行b.mjs的时候,已知它从a.mjs输入了foo接口,这时不会去执行a.mjs,而是认为这个接口已经存在了,继续往下执行。执行到第三行console.log(foo)的时候,才发现这个接口根本没定义,因此报错。

如何解决

b.mjs运行的时候,foo已经有定义了。这可以通过将foo写成函数来解决。

// a.mjs

import {bar} from './b';

console.log('a.mjs');

console.log(bar());
function foo() { return 'foo' }
export {foo};


// b.mjs`
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};

这时再执行a.mjs,就可以得到预期结果,这是因为函数具有提升作用,在执行import {bar} from './b'时,函数foo就已经有定义了,所以b.mjs加载的时候不会报错。这也意味着,如果把函数foo改写成函数表达式,也会报错。

CommonJS与ESModule

因为现代的构建工具(如Webpack)和运行时环境(如Node.js)通常会支持跨模块化系统的互操作性。当使用 import countObj from './countObj.js'; 语法时,构建工具会自动将其转换为适合目标环境的模块化规范(例如babel-loader)。

否则模块化方案不同是不能直接互相引的!!!!!

使用CommonJS导出ESModule

// 导出  name.js
export const age = 18;
export default "这是一个默认导出";

// 引入
let info = require("./name");
console.log(info);

使用ESModule导出CommonJS

// 导出  countObj.js
const x = 5;

const add = function (a) {

  return a + x;

};

module.exports.x = x;
module.exports.add = add;

// 引入
import countObj from './countObj.js';

console.log(countObj);

注:通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。混用的话容易导致问题

require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React';

实际应用场景与问题

使用CommonJS规范打包一个SDK(第三方库),常用与node服务端或者小程序场景

// 使用webpack打包       webpack.config.js配置
const path = require('path');
const webpack = require('webpack');
module.exports = {
    mode: 'development',
    entry: './src/main.js',
    output: {
        path: path.resolve(__dirname, './dist'),
        publicPath: '/dist/',
        filename: 'lodashshsh.js',
        libraryTarget: 'lib'
    },
    devtool: 'source-map'
    
};

// 引入该sdk
const lodashshsh = require('./dist/lodashshsh.js');
console.log(lodashshsh);
// rollup打包    rollup.config.js配置
export default {
  input: './src/main.js',
  output: {
    file: './dist/lodashshsh.js',
    format: 'cjs'
  }

};

项目分别打包ES Module文件、CommonJS文件、UMD文件(经典例如ant-design)

简单写下配置代码,在使用babel的情况下,仅供参考,实际问题实际解决

// CommonJS
{
  "presets": [
    ["@babel/env", {
      "loose": true,
      "modules": "cjs"
    }], "@babel/preset-react"
  ],
  "plugins": [
    ["@babel/plugin-transform-runtime", {
      "useESModules": false
    }],
  ]
}


// ES Modules
{
  "presets": [
    ["@babel/env", {
      "loose": true,
      "modules": false
    }], "@babel/preset-react"
  ],
  "plugins": [
    ["@babel/plugin-transform-runtime", {
      "useESModules": true
    }]
  ]
}

// UMD 
{
  "presets": [
    ["@babel/preset-env", {
      "modules": false,
    }], "@babel/preset-react"
  ],
  "plugins": [
    ["@babel/plugin-transform-runtime", {
      "useESModules": false
    }]
  ]
}

ant的除了 es modules,还打包生成了 commonjs 也就是它的 lib 文件夹,还有最落后的 umd 也就是 dist 文件夹,也就是浏览器可以直接引用使用的

注:这里面有个容易纠结的点,例如日常开发中的Vue、React单页面应用(网页),它的打包后产物,是能够在浏览器上直接打开(打不开一般都是路径的问题)或者服务器上直接部署的静态资源。对于这个资源来说,因为不作为模块被引用,虽然它是UMD规范,也可以不用纠结它的规范(因为没意义)。

而ant-design中除了 es modules,还打包生成了 commonjs 也就是它的 lib 文件夹,还有 umd 也就是 dist 文件夹,也就是浏览器可以直接引用使用的。

常见问题

ES Module和CommonJS的差异

CommonJS:

CommonJS 模块是运行时加载

CommonJS 模块输出的是一个值的拷贝

CommonJs 是单个值导出,本质上导出的就是 exports 属性。

CommonJS 是可以动态加载的,对每一个加载都存在缓存,可以有效的解决循环引用问题。

CommonJS 模块同步加载并执行模块文件。

ESModule:

ES6 Module 静态的,不能放在块级作用域内,代码发生在编译时。

ES6 Module 输出的是值的动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。可以通过导出方法修改,可以直接访问修改结果。

ES6 Module 可以导出多个属性和方法,可以单个导入导出,混合导入导出。

ES6 模块提前加载并执行模块文件,

ES6 Module 导入模块在严格模式下。

ES6 Module 的特性可以很容易实现 Tree Shaking 和 Code Splitting。

参考文章

  1. 前端模块化详解(完整版)
  2. 使用模块化工具打包自己开发的JS库(webpack/rollup)对比总结
  3. 使用umd、commonjs和es三种模式制作自己的React 组件(库)
  4. 从构建产物洞悉模块化原理

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYgEmvEk' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片