Script Module 学习

116 阅读5分钟

前情

很久以前,前端页面功能简单,浏览器运行单一js,没有模块的概念。后面复杂起来后,出现了很多模块处理的机制

  • CommonJs
// 通过require 引入module
require('xxx');

// exports 当前module导出的功能
exports.xxx = xxx;

同步的形式加载模块,主要是node使用

  • AMD - Requirejs
// 定义模块一个
define(['a'], (a) => {
    // 导出的功能
    return {
        d: 'xxx'
    };
});

// 引入模块
require(['b', 'c'], (b, c) => {});

异步的形式加载模块,浏览器加载js一般都是异步的,所有AMD一般在浏览器端使用。

  • CMD - sea.js

CMD是AMD的一种优化。Requirejs在申明依赖的时候就会加载并运行依赖。

// AMD
define(['a', 'b', 'c'], (a, b, c) => {
    if (false) {
        // 即便没用到某个模块 c,但 c 还是提前执行了
        a();
    }
});
// CMD
define(function(require, exports, module) {
    var a = require('./a'); //在需要时申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

AMD推崇依赖前置、提前执行

CMD推崇依赖就近、延迟执行

  • UMD

UMD是commonjs、AMD的兼容写法,这样打包出来的功能,可以在两种模块机制下运行。

((root, factory) => {
  if (typeof define === 'function' && define.amd) {
    //AMD
    define(['jquery'], factory);
  } else if (typeof exports === 'object') {
    //CommonJS
    var $ = requie('jquery');
    module.exports = factory($);
  } else {
    //都不是,浏览器全局定义
    root.testModule = factory(root.jQuery);
  }
})(this, ($) => {
  //do something...  这里是真正的函数体
});
  • ES6 module
import xxx from 'xxx';

export {}
export default xxx

es6 module出现后,上面的都变成时代的眼泪了。

目前使用es module还需要依赖打包工具,打包成单一的js。打包工具内部也实现了一套 模块加载机制。

Script module

浏览器原生支持的模块加载

基本用法

基本用法-官方DEMO

// 加载资源 xxx.js,xxx.js 内部可以用 import 和 export 加载到导出模块
<script type="module" src="./xxx.js"></script>
// 直接在script标签内,import module
<script type="module">
  import xxx from './xxx.js'
  // do something
</script>
import a from './a.js'; // 可以在浏览器看到 网络请求了 a.js 资源

特点:
  • 用法和es6 module相同
  • import的时候需要带后缀。
  • type="module"script标签自带defer属性,会在dom运行结束才运行。
  • import的模块只会被加载和执行一次。
  • import内部定义的变量只在内部可以被使用,export出去的只有import的时候才能获取,window上是没有的。
动态加载

动态加载-官方DEMO

// 动态加载
import('xxx').then((xxx) => {
    // do something
})

执行到的时候才会加载对应的module,加载成功后Promise.resolve

Top Level await

官方DEMO

// color.js
// fetch request
const colors = fetch("../data/colors.json").then((response) => response.json());

export default await colors;
// main.js
import colors from "./modules/getColors.js";

// do something
循环引用

循环引用的时候不是总是有问题的。

// -- a.js --
import { b } from "./b.js";

setTimeout(() => {
  console.log(b); // 1
}, 10);

export const a = 2;

// -- b.js --
import { a } from "./a.js";

setTimeout(() => {
  console.log(a); // 2
}, 10);

export const b = 1;

这个例子里面,异步调用,是不会有问题的。

  1. import的时候只是对export的一个引用
  2. 真的调用的时候 export语句已经运行完了。
// -- a.js (entry module) --
import { b } from "./b.js";

export const a = 2;

// -- b.js --
import { a } from "./a.js";

console.log(a); // ReferenceError: Cannot access 'a' before initialization
export const b = 1;

这个例子就无法运行。

  1. 运行a.js第一句 import { b } from "./b.js"
  2. 加载并执行 b.js
  3. b.js第一句import { a } from "./a.js";a.js已经被导入了,不会再次运行。
  4. console.log(a),需要调用 a.js的导出,这时候 a.jsexport语句还没被执行,所有会报错。
// -- a.js (entry module) --
import { b } from "./b.js";

console.log(b); // 1
export const a = 2;

// -- b.js --
import { a } from "./a.js";

setTimeout(() => {
  console.log(a); // 2
}, 10);
export const b = 1;

这个例子是可以运行的。

  1. 运行a.js第一句 import { b } from "./b.js"
  2. 加载并执行 b.js
  3. b.js第一句import { a } from "./a.js";
  4. 设置定时器。
  5. b.js导出 b=1;
  6. 执行a.jsconsole.log(b);,因为b.js已经执行过export了,运行正常。
  7. a.js导出a=1
  8. b.js的定时器运行,此时a.js已经export过了,所有可以正常访问。

Script importmap

浏览器加载模块的配置,控制对应模块的加载地址。(有点类似alias)

格式

内部是个JSON格式,支持importsscopes两个key

imports path 的匹配模式

  • 裸模块
<script type="importmap">
  {
      imports: {
          lodash: 'https://xxxxx',
          moduleA: './assets/module.123gde.js',
      }
  }
</script>

js内部就可以直接import

import _ from 'lodash'; // 等同于 import _ from 'https://xxxxx';
import moduleA form 'moduleA'; // 等同于 import moduleA from './assets/module.123gde.js';
  • 前缀匹配
<script type="importmap">
 {
     imports: {
         "shapes/": "./module/shapes/"
     }
 }
</script>
import circle from 'shapes/circle.js' // 等同于 import circle from './module/shapes/circle.js'
  • 路径匹配
<script type="importmap">
{
  "imports": {
    "modules/shapes/": "./module/src/shapes/",
    "modules/shapes/square": "./module/src/other/shapes/square.js",
    "https://example.com/modules/square.js": "./module/src/other/shapes/square.js",
    "../modules/shapes/": "/modules/shapes/"
  }
}
</script>

一个请求如果匹配多个path的时候,取最长的。eg:

import 'modules/shapes/square'; // 等同于 import './module/src/other/shapes/square.js';
import 'modules/shapes/a.js'; // 等同于 import './module/src/shapes/a.js';

scopes

scopes配置的是资源请求的路径匹配,然后对返回的资源内部的资源请求进行module转换。

scopes一般用来做版本控制

<script type="importmap">
    {
        "imports": {
          "box": "./src/b.js"
        },
        "scopes": {
          "custom/": {
            "box": "https://example.com/modules/shapes/square.js"
          }
        }
    }          
</script>
import './src/a.js';
import './custom/a.js';
// src/a.js
import 'box'; // 等同于 import './src/b.js';
// custom/a.js
import 'box'; // 等同于 import 'https://example.com/modules/shapes/square.js';

拓展

随着前端业务的发展,项目越来越大,微前端被提了出来。

Webpack5提供了一个新功能 - Module Federation

Module Federation就是Webpack利用自己的模块处理机制实现的。针对配置了remote的module,采用异步chunk的形式去加载。

Module Federation的形式很好的对大型的系统进行 切分,独立的部署,组合使用。

但是 想要使用Module Federation依赖项目都使用 Webpack5进行打包。

script module其实是很好的Webpack Module Federation的替代。

  1. 子项目打包成功后,生成一个 import-map.json
  2. 父项目在html引入import-map.json,父项目直接import子项目导出的功能。
例子实践

Live Demo

github 源码


功能:

  • web-components导出了2个webcomponent组件。
  • app直接使用这两个 webcomponent组件。

步骤:

  1. web-components项目 build之后根据vite生成的mainfest.json一个import-map.js的文件。
  2. app项目在入口index.html引入这个import-map.js
  3. app项目内部直接 importweb-components项目导出的 webcomponent,直接使用~

参考资料: