一、文章参考

  1. 在浏览器中使用JavaScript module(模块)
  2. script 标签 MDN

二、基本语法

2.1 快速入门

<!-- 方式1: 内联模块 -->
<script type="module">
  import { greet } from './utils.js';
  greet('World');
</script>

<!-- 方式2: 外部模块文件 -->
<script type="module" src="main.js"></script>

2.2 引用要有扩展名和完整的URL路径

// ❌ 错误的导入方式
import utils from './utils'           // 缺少文件扩展名
import '../module'                     // 缺少文件扩展名
import { foo } from 'library'          // 需要完整的URL或配置

// ✅ 正确的导入方式
import utils from './utils.js'        // 必须有扩展名
import config from '/src/config.js'    // 绝对路径
import lodash from 'https://cdn.skypack.dev/lodash' // 完整URL

// 相对路径基准是当前HTML文件位置
import './lib/module.js'  // 相对于HTML文件

三、核心特性与例子

1. 模块导入/导出

// math.js - 导出模块
export function add(a, b) {
  return a + b;
}

export const PI = 3.14159;

export default function multiply(a, b) {
  return a * b;
}

// main.js - 导入模块
import multiply, { add, PI } from './math.js';

console.log(add(2, 3));     // 5
console.log(multiply(2, 3)); // 6
console.log(PI);            // 3.14159

2. 严格模式自动启用

// type="module" 中自动启用严格模式
// 以下代码会报错(非模块脚本不会报错)
x = 10; // ReferenceError: x is not defined
delete Object.prototype; // TypeError

3. 作用域隔离\模块作用域,不会污染全局

<script type="module">
  const message = "模块内部";
  console.log(message); // "模块内部"
</script>

<script type="module">
  console.log(message); // ReferenceError: message is not defined
</script>

<script>
  console.log(message); // ReferenceError: message is not defined
</script>
<script type="module">
  // 模块作用域,不会污染全局
  const privateVar = 'secret'
  
  // 需要显式导出到全局
  window.myModule = { 
    publicMethod: () => console.log(privateVar)
  }
</script>

<script>
  // 传统脚本不能访问模块变量
  console.log(privateVar)  // undefined
  window.myModule.publicMethod()  // 可以访问
</script>

4. 延迟执行(Defer)

<!-- 模块脚本自动延迟执行 -->
<script type="module">
  console.log('模块执行'); // 会在HTML解析后执行
</script>

<script>
  console.log('普通脚本'); // 可能先执行
</script>

5. 跨域限制

<!-- 需要正确的CORS头部 -->
<script type="module" src="https://other-site.com/module.js"></script>
<!-- 需要服务器设置: Access-Control-Allow-Origin: * -->

6. 动态导入

// 按需加载模块
button.addEventListener('click', async () => {
  const module = await import('./heavyModule.js');
  module.doSomething();
});

四、实际项目结构示例

project/
├── index.html
├── main.js          # 入口文件
└── modules/
    ├── api.js       # API模块
    ├── utils.js     # 工具函数
    ├── ui.js        # UI组件
    └── config.js    # 配置
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>模块化应用</title>
</head>
<body>
  <script type="module" src="main.js"></script>
</body>
</html>
// main.js - 入口文件
import { initApp } from './modules/app.js';
import { loadConfig } from './modules/config.js';

(async () => {
  const config = await loadConfig();
  await initApp(config);
})();

五、type="module " 的优势

1. 代码组织

// 传统的非模块方式 - 全局污染
var utils = {}; // 全局变量
utils.formatDate = function() { /* ... */ };

// 模块方式 - 清晰的依赖关系
import { formatDate } from './dateUtils.js';
import { validateEmail } from './validation.js';

2. 依赖管理清晰

// package.json + 模块导入
import lodash from 'lodash-es'; // ES模块版本
import axios from 'axios';

3. 浏览器缓存模块

// 模块只会执行一次
// module.js
console.log('模块执行')

// main1.js
import './module.js'  // 输出: "模块执行"

// main2.js
import './module.js'  // 无输出,已缓存

// 强制重新加载(不推荐)
const freshModule = await import('./module.js?v=' + Date.now())

4. 顶层await的限制

// ✅ 允许顶层await
const response = await fetch('/api/data')
const data = await response.json()

// ❌ 在非模块脚本中不允许
// <script>  // 传统脚本中会报错
//   await fetch()  // SyntaxError
// </script>

5. 多个模块合并为一个模块

// ❌ 避免过多的小模块请求
import tinyUtil1 from './util1.js'  // 1KB
import tinyUtil2 from './util2.js'  // 1KB
import tinyUtil3 from './util3.js'  // 1KB
// 导致3个HTTP请求

// ✅ 合并相关模块
// utils/index.js
export { default as util1 } from './util1.js'
export { default as util2 } from './util2.js'
export { default as util3 } from './util3.js'

// main.js
import { util1, util2, util3 } from './utils/index.js'

5. 查看模块依赖

// 在浏览器控制台
import.meta.url      // 当前模块URL
import.meta.resolve('./module.js') // 解析模块路径

使用场景

1. 现代Web应用

// 单页面应用(SPA)
import { createApp } from 'vue';
import router from './router';
import store from './store';

createApp(App).use(router).use(store).mount('#app');

2. 组件库开发

// 组件库入口
export { default as Button } from './components/Button.svelte';
export { default as Modal } from './components/Modal.svelte';
export { default as Input } from './components/Input.svelte';

3. 工具函数库

// utils/index.js
export * from './string-utils.js';
export * from './array-utils.js';
export * from './date-utils.js';
export * from './validation.js';

4. 第三方库的ES模块版本

<!-- 直接使用ES模块CDN -->
<script type="module">
  import { html, render } from 'https://unpkg.com/lit-html?module';
  
  render(html`<h1>Hello Module!</h1>`, document.body);
</script>

注意事项

1. 浏览器支持

  • 现代浏览器都支持
  • 旧浏览器需要构建步骤(Webpack、Vite等)

2. 文件路径

// 需要明确的文件扩展名
import './module.js';    // ✅ 正确
import './module';       // ❌ 错误(在原生模块中)

// 需要正确的路径
import './modules/utils.js';           // ✅ 相对路径
import '/src/utils.js';                // ✅ 绝对路径
import 'https://site.com/module.js';   // ✅ 完整URL

3. 与普通脚本的区别

<!-- 模块脚本 -->
<script type="module">
  // 自动defer,作用域隔离,支持import/export
</script>

<!-- 普通脚本 -->
<script>
  // 可能阻塞渲染,全局作用域
</script>

4. 开发环境配置

// package.json
{
  "type": "module", // Node.js中也使用ES模块
  "scripts": {
    "dev": "vite", // 使用支持原生ES模块的构建工具
    "build": "vite build"
  }
}

实时示例

<!DOCTYPE html>
<html>
<head>
  <title>模块示例</title>
</head>
<body>
  <button id="load">加载模块</button>
  <div id="output"></div>

  <script type="module">
    // 主模块
    import { showMessage } from './messageModule.js';
    
    document.getElementById('load').addEventListener('click', async () => {
      // 动态导入大型模块
      const chartModule = await import('./chartModule.js');
      chartModule.renderChart();
      
      showMessage('模块加载成功!');
    });
  </script>
</body>
</html>
// messageModule.js
export function showMessage(text) {
  document.getElementById('output').textContent = text;
}

// chartModule.js
export function renderChart() {
  console.log('渲染图表...');
}

总结

type="module" 是现代JavaScript开发的基础,它提供了:

  1. 更好的代码组织 - 模块化架构
  2. 明确的依赖关系 - 静态分析支持
  3. 作用域隔离 - 避免全局污染
  4. 现代特性支持 - 顶层await、动态导入等
  5. 开发体验提升 - 结合Vite等工具实现快速HMR

对于新项目,强烈推荐使用ES模块作为标准开发方式。

二、script 标签的行为

2.1 默认行为(无任何修饰)

  1. 浏览器会立即加载并执行指定的脚本
  2. 会阻塞 HTML 文档解析,并按照它们出现的顺序执行

2.2 <script async >

  • 特性
    • async 仅适用于外链,规定脚本异步执行
    • 下载不会阻塞页面解析
    • 脚本的加载完成后就马上执行,脚本执行时会阻塞 HTML 解析
    • 不会按照出现的顺序执行,先下载完成哪个就先执行哪个
    • 执行的时候,有可能页面还没解析完成
    • DOMContentLoaded事件的触发并不受async脚本
    • 加载的影响。
  • 脚本下载与 HTML 解析并行,但一旦脚本加载完成,就会中断 HTML 解析,同时执行脚本

2.3 <script defer >

  1. 特性
    • defer 仅适用于外链,规定脚本延迟执行
    • 不会阻塞页面解析
    • 会按照出现的顺序执行
    • 在 HTML 解析完成后, DOMContentLoaded 之前执行
  2. 脚本下载与 HTML 解析并行,等 HTML 解析完成后每个脚本都会有序执行

2.4 <script type=“module” >

在script标签中写js代码,或者使用src引入js文件时,默认不能使用module形式,即不能使用import导入文件,但是我们可以再script标签上加上type=module属性来改变方式

type=“module” 与 async、defer修饰符同时使用

  1. <script type="module"> 为类似于 <script defer> ,但是模块依赖关系也会被下载

  2. <script type=“module” async> 行为类似于 <script async> ,额外的模块依赖关系也会被下载

案例 —— 在HTML中像ES6编码方式编程

  1. demo.html
<html>
 <head>
  <script src='main.js' async type="module"></script>
 </head>
 <body>
  <a class="btn">Search</a>
 </body>
</html>
  1. main.js
import Movie from "./movie.js";

const btnSearch = document.querySelector(".btn");

btnSearch.addEventListener("click", function() {
  let filmeTeste = new Movie(
    "The Lego Movie",
    2014,
    "tt1490017",
    "Movie",
    "https://m.media-amazon.com/images/M/MV5BMTg4MDk1ODExN15BMl5BanBnXkFtZTgwNzIyNjg3MDE@._V1_SX300.jpg"
  );
  console.log(`object test ${filmeTeste}`);
});
  1. movie.js
class Movie {
  constructor(title, year, imdbID, type, poster) {
    this.title = title;
    this.year = year;
    this.imdbID = imdbID;
    this.type = type;
    this.poster = poster;
  }
}
export default Movie;
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐