js 中的模块化

模块化


概念

​ 一个模块就是 一个文件,一个脚本 。模块间可以相互加载,使用 importexport 关键之来进行 导入 和 导出 。

  • export: 导出 / 暴露 当前模块的 变量 或 函数 。

  • import:导入 另一个模块暴露的 变量 或 函数 。

    有2中模式:

    // 1.导入特定的变量 或 函数
    import { Button } from './xxx.js'
    
    // 2.单导入一个文件
    import './xxx.js'
    

例子:

我们在 sayHi.js 文件中,导出 一个函数;

export function sayHi () {
    console.log('Hello 我是sayHi.js 的函数')
}

在 main.js 中导入这个函数;

import { sayHi } from './sayHi.js'

sayHi()

要使用 ES6 环境来运行,才可以 。



在 html 页面导入模块:

注意,在 html 页面中使用模块的 js 语法,使用导入模块的 关键字(import),在进入 js 文件时,要给 <script> 标签添加一个属性,告诉浏览器,此脚本应该被当作模块 来对待 。

1,在这个文件中,我们导出一个方法:

say.js

export function sayHi () {
    console.log('Hello 我是sayHi.js 的函数')
}

2,在 html 页面使用这个方法:

<body>
<script type="module">
    import { sayHi } from './say.js'
    sayHi()
</script>
</body>

模块只通过 HTTP(s) 工作,在本地文件则不行

以上的代码不能使用 本地浏览 html 页面来查看效果,要使用 服务器来访问页面。如 vs code 的 serve 插件挂载页面,然后进行访问,才不会报错 。

在这里插入图片描述



模块的核心功能:

1. 模块使用 use strict

模块默认使用 “ 严格模式 ”;

如,变量没有创建,就赋值,会报错!

<script type="module">
var a = 12;  // a is not defined
console.log(a)

</script>

2. 模块作用域

每个模块都有自己的 顶级作用域;一个模块中的 变量 和 函数,在另一个 模块 中是访问不到的(除非模块进行了导出)。

one.js
export let one = '我是 one 文件的变量'

tow.js
console.log(one)

index.html
<body>
    <script type="module" src="./one.js"></script>
    <script type="module" src="./two.js"></script>
</body>

我们浏览 index 页面是这样的:

在这里插入图片描述

因为我们在 two.js 中使用 one.js 模块的变量,会报错,因为我们没有在 two.js 中导入 one.js 模块 。

我们把在 two.js 中导入 one 看看:

two.js
import { one } from './one.js'

console.log(one)

在这里插入图片描述



在同一个文件中的 script 也存在 模块作用域 :

<body>
<!-- 在同一个文件中的 script 也存在 模块作用域 -->
<script type="module">
    let one = '你好'
</script>


<script type="module">
console.log(one)  // one is not defined
</script>

</body>


3. 模块的解析

模块 在第一次被导入到文件时,文件只会在第一次导入 模块 时,对模块进行 解析(执行)。

注意:

  • 同一个文件,导入 同一个模块时,只对模块 执行一次;

这个很重要哦 。


例子1:

我们创建 one.js 文件,他会在 控制台输出一串字符串,我们在多个文件中导入这个 模块;看看他的触发情况:

one.js
console.log('我是 one 导出的方法')

two.js
import './one.js'

three.js
import './one.js'

然后我们在 index.html 页面中导入3个文件;

<body>
    <script type="module" src="one.js"></script>
    <script type="module" src="two.js"></script>
    <script type="module" src="three.js"></script>
</body>

因为 导入模块时,只对第一次导入时,对模块进行执行;再次导入时,是不会对模块进行 执行的 :

在这里插入图片描述


例子2:

第一次导入模块时,对模块里的数据进行修改;然后在另一个文件中,导入同一个模块,读取刚刚修改的数据,值为第一次导入模块时,修改后的数据 。

one.js

export let one = {
  name: 'one'
}

two.js

导入 one 模块,并修改了他里面的值

import { one } from './one.js'

one.name = '我是 two, 修改了 one 的 name'

three.js

导入模块,并输出模块的内容

import { one } from './one.js'
console.log(one)

都在 index.html 页面导入

<body>
    <script type="module" src="one.js"></script>
    <script type="module" src="two.js"></script>
    <script type="module" src="three.js"></script>
</body>

输出的 one.name 值为被修改过的数据:

在这里插入图片描述


如果 模块 不止 加载一次,那么,我们在 three.js 中输出的值为 one.js 原来的值 ;

因为重新加载模块,会把

  • one.js 重新加载一遍,
  • 重新创建,导出一个 one 的变量,
  • 重新给 one 变量 赋值一个 对象
  • 最终输出的值,就为 'one'
one.name => 'one'

提问:模块只加载一次的前提,是在同一个文件中引入吗?

实验: 在不同的文件中导入同一个模块,在第一个文件中修改模块的值,然后在第二个文件中看看,这个模块是值,是否被修改了呢?

不会,因为不在同一个文件导入模块,所以他们没有关系呢!


还是上面的 one.js, two.js, three.js; 我们在 1.html 导入 3个文件;在 2.html 导入 three.js 文件,

我们的 two.js 改变了 one.js,但在 2.html 中,我们并没有调用他们,所以导入的 three.js 输出的值没有变化 。

<body>
  <script type="module" src="./three.js"></script>
</body>



4. import.meta

import.meta 对象包含关于当前模块的信息,它的内容取决于其所在的环境 。

在浏览器环境中,它包含当前脚本的 URL

<script type="module">
    console.log(import.meta)
</script>

如果它是在 HTML 中的话,则包含当前页面的 URL 。

// xxx.js
console.log(import.meta)


5. 在一个模块中,“this” 是 undefined


6. 模块脚本是延迟的

模板脚本 的加载和 script 标签的 defer 属性一样;

  • 外部模板脚本 和 html 页面同时进行加载;
  • 模块脚本,会在 html 页面加载好后执行;
  • 排在前面的 模块脚本 先 执行;

先说一个知识点,就是给元素添加 id 属性,我们可以使用 id名,直接获取到元素 :

<body>
    <button id="button">按钮</button>
</body>

<script>
    console.log(button) 
    // => <button id="button">按钮</button>
</script>

开始例子,不是说使用 module 的模块会延迟按下吗!以下是验证:

<script type="module">
    console.log(1, button)
</script>

<script>
    console.log(2, typeof button)
</script>

<button id="button">按钮</button>


<script>
    console.log(3, button)
</script>

html页面加载,从上往下执行,

  1. 执行第 2 个 输出语句;
  2. 加载到 按钮标签;
  3. 执行第 3 个输出语句;
  4. 最后,html 加载好后,我们的 模块 才开始执行;

在这里插入图片描述


所以,有一个问题:

html 页面在加载时,就会显示出来;而我们的模块 是在 html 页面加载好后,才开始执行的;

如果我们的页面中的某个元素,使用了 模块的功能,但是在 html 加载时,这个元素显示到页面上了;但是 html 页面还没有加载好,模块还没有执行,所以这个 元素可能起不到原来设想的效果 。我们要在页面还未加载完成之前,弹出一个提示文本框,提示页面加载中。确保不会使用户感到困惑。


7. 模块使用 async

对于非模块脚本,async 仅使用外部脚本;

但对于 模块脚本,他使用与 内联脚本:

我们给 模板脚本 添加 async 属性,可以实现 async,同步加载的效果,加载好后,立即执行 模板脚本文件 。

对一些不依赖任何其他东西的功能模块是最好的:如 计时器,广告,文档监听器 等等;

<body>
    <script async type="module">
        console.log('我自己,加载好后就立即执行')
    </script>
</body>


8. 外部脚本

具有 type="module" 的外部脚本(external script)在两个方面有所不同:

1,具有相同 src 的外部脚本仅运行一次:

2,如果一个模块脚本是从另一个源获取的,则远程服务器必须提供表示允许获取的 header Access-Control-Allow-Origin。(不懂)

<!-- another-site.com 必须提供 Access-Control-Allow-Origin -->
<!-- 否则,脚本将无法执行 -->
<script type="module" src="http://another-site.com/their.js"></script>


9. 不允许裸模块

在浏览器中,import 必须给出相对 或 绝对地址的路径;

没有任何路径的模块称为 “裸” 模块;在 import 中不允许这种模块 。

例如:

import { xx } from 'xx'  // Error

import { xx } from './xx.js'  // 如

但在 node 或 打包工具 中,允许没有任何路径的模块;因为他们有进行处理过的 。



10. 兼容性 nomodule

旧时的浏览器不理解 type="module"。未知类型的脚本会被忽略。对此,我们可以使用 nomodule 特性来提供一个后备 (不太懂)

参考: https://zh.javascript.info/modules-intro#shi-zhong-shi-yong-usestrict



导出 和 导入:

1. 在声明前导出:

one.js 导出:

export let name = 'zyf'

export const arr = [1,2,3,4,5]

export function Fn () {
  console.log(1 + 5)
}

任何在 two.js 中导出:

import { name, arr, Fn } from './one.js'

console.log(name)
console.log(arr)
Fn()

因为运行环境是在 浏览器 中,所以都在 页面 中,使用 module 属性导入文件:

<body>
    <script type="module" src="one.js"></script>
    <script type="module" src="two.js"></script>
</body>


2. 导出 和 声明 分开:

先声明变量 或 函数,数组 等等数据;然后再 文件的底部导出,我们想对外面 暴露 的 数据 ;

// one.js
function aa() {
  console.log('我是aa啊!')
}


function bb () {
  console.log('我是 one 文件的函数哦')
}

export {
  aa,
  bb
}

然后再 two.js 引用数据

import { aa, bb } from './one.js'

aa()   // => 我是aa啊!
bb()  // => 我是 one 文件的函数哦


3. import *

我们一般使用 export 导出的数据,在另一个文件中导入时;

我们要写对应的变量 来接收 他们(名字要一致哦);

比如是这样的:我们要一个一个去导入,好像必须麻烦

one.js

function aa() {
  console.log('我是aa啊!')
}


function bb () {
  console.log('我是 one 文件的函数哦')
}

export {
  aa,
  bb
}

two.js

import { aa, bb, arr } from './one.js'

aa()
bb()
console.log(arr)

但是我们可以使用 import * 来导入:显得比较容易

import * as xx from './one.js'

xx.aa()
xx.bb()
console.log(xx.arr)

as 用于起别名 。


但是!但是!我们通常不会使用这种写法!为什么呢?

答:

1,在构建工具 中,会将模块打包在一起,然后对其进行优化,以加快 加载速度,并删除未使用的代码 。

如,我们有一个文件,他里面导出了很多方法:

// say.js
export function sayHi() { ... }
export function sayBye() { ... }
export function one() { .... }

我们在 two.js 中使用到 say.js 方法:

// two.js
import { one } from './say.js'

构建工具的 优化器 就会检测到我们导入的方法;并从打包好的代码中 删除 那些我们没有使用的方法,从而使 构建好的文件 变得更小;这样称为 “摇树” 。


2,在 导入的列表 可以更好的看出当前这个模块 依赖某个模块中的哪些方法!



4. import “as”

导入文件时,可以对 导入的数据 起别名,尤其对于一些导入数据名 比较长的 数据,起别名是一个好的方法 。

// 📁 two

import { aa as one , bb as two, arr as three } from './one.js'

one()
two()
console.log(three)


5. export “as”

我们在导出时,也可以使用 as ,来对导出的数据起 别名 。

在 one.js 中对导出的 变量起 别名 。

// 📁 one.js

function aa() {
  console.log('我是aa啊!')
}


function bb () {
  console.log('我是 one 文件的函数哦')
}

const arr = [1,2,3,4,5]

export {
  aa as one,
  bb as two,
  arr as three
}

在 另一个文件中导入:

// 📁 two.js
import { one , two, three } from './one.js'

one()  // => 我是aa啊!
two()  // => 我是 one 文件的函数哦
console.log(three)  // => [1,2,3,4,5]


6. export default

实际中,模块的导出有 2 中方法:

  • 命名的导出

    export let aa = '默认导出,一个文件能有多个'
    

    导入时,需要使用 {} 花括号来 告诉 import 我们需要导入的是那个值;

  • 默认的导出

    export default function fn () {
        console.log('我是默认导出的,一个模块,只能有一个,命名导出 和 默认导出 在文件中最好不要混合使用 。')
    }
    
    // 也可以这样
    function fn() {
        xxxxx
    }
    
    export default fn
    

    导入时,不需要使用 {} 花括号,因为导入的模块文件,只有一个导出的值;我们还需要给 导入的模块 起一个变量名 接收 导入的值;

    import nihao from './xxx.js'
    

默认导出,就是让一个模块,只做一件事情;这样更好 。

因为 默认导出 在模块中,只有一个,所以呢?我们导出的数据 可以不起名字,因为我们导出的 只有一个,到导入中 import 知道我们要导入的是什么 !

// 📁 one.js
export default '你好,我是使用默认导出的字符串'
// 📁 two.js
import str from './one.js'

console.log(str) // => '你好,我是使用默认导出的字符串'

但是,如果不是使用 默认导出 的,导出时,要写名字:

// 📁 one.js
export  '你好,我是使用默认导出的字符串'  // 错误

7. default 名称

1,我们使用这种方式,也是 默认导出,有点特别:

// 📁 one.js
function fn () {
  console.log('你好,我是使用默认导出的')
}

export {fn as default}

样子看上去像 命名导出,但实际上的 默认导入,我们在导入时,就知道了:

// 📁 two.js
import fn from './one.js'

fn()

使用 {} 会报错 !


2,默认导出 和 命名导出 出现在同一个模块中:

// 📁 one.js
export let str = '你好,我是使用命名导出的字符串'

function fn () {
  console.log('我是使用默认导出的函数')
}
export default fn

这种情况比较少吧,我们可以这样导入:

// 📁 two.js
import fn, { str } from './one.js'
fn()   // => '我是使用默认导出的函数'
console.log(str) // => '你好,我是使用命名导出的字符串'


// 或者可以这样, fn 是自定义的
import { default as fn, str } from './one.js'
aa()
console.log(str)


// 或者是这样
import * as one from './one.js'
console.log(one.str)
one.default()  // 这个就是 导出的 fn 函数


8. 我们该使用哪种导出方式?

他们都有各自的好处:

命名导出,导入的名字 要和 导出的名字一致,不一致会导出报错!
默认导出不会,但多人开发,有时,会导出 同一个文件,多人在自己的模块中,导入时,起了不同的名字,导致混乱(默认导出不背锅)。


9. 重新导出

“重新导出” 的语法为

export { xx } from 'a.js'

// 相对于以下代码

import { xx } from 'a.js'
export { xx }

a.js 文件中的数据导入到 当前的文件中,并将这个在 a.js 导入的数据,再次导出 ;

!!!

你你你,为什么要这样呢?为什么要导出到 当前这个文件,然后再导出呢?

明明导出 a.js 到我们想要使用的文件中不就好了吗?

咳咳!是啊,但在某个场景下,这种做法很有用哦!


背景

  1. 假设 a 文件夹,里面是有很多文件,整个文件夹 都是 一些封装好的方法,我们可以去调用里面的方法,来实现某些功能;

    a文件夹:
        index.js    // 入口文件,我们要实现的
        add.js     // 实现数据累加的方法
        update.js  // 实现数据修改的方法
        remove.js  // 实现数据删除的方法
        user.js
        ......
    

  1. 里面的方法们各种都是 互相有联系的,就会相互导入和导出方法;

    // 📁 update.js
    function updatexx () {
        xxxxxx
    }
    export { updataxx }
    
    
    // 📁 add.js
    import { updatexx } from './update.js'
    
    // add 中的一些方法
    function addxx () {
        xxx
    }
    export { addxx }
    

  2. 整个文件夹里,有一些是对外提供的方法,有一些是内部实现的方法,不应该被用户调用。


  1. 我们就可以创建一个文件,当做这个 封装好的方法(整个文件)的 入口文件,在这个文件上 导出我们对用户提供的方法 。

    // 📁 index.js
    
    // 导入 add 中的方法
    import { add, addxx } from './add.js'
    // 然后导出
    export { add, addxx }
    
    // 导入 update 中的方法
    import { updatexx } from './update.js'
    export { updatexx }
    

    使用 export ... from ... 简化:

    export { add, addxx } from './add.js'
    export { updatexx } from './update.js'
    

    为什么要呢?在这个文件中,导入,还要导出?

    因为我们使用, a 文件夹,这个方法的用户只要导入一个 /a/index.js 就可以了!不用导入用到方法的文件 。

    然后再使用 {} 花括号,导出这个文件中想要的方法,就可以了,


    用户使用:

    // 📁 用户文件
    
    /* 使用 方法中的 addxx 这个方法,因为我们在 index.js 中导入了 add.js 的 addxx 方法,又导出了,
    所以我们导入 index.js 文件,就可以得到这个方法了,而不用去导入 './a/add.js' 文件夹来操作,
    还得知道你这个文件的位置,才能导入呢? 是不是方便了很多呢! */
    
    import { addxx, updatexx } from './a/index.js'
    

    不然要这个导入 addxxupdatexx 这2个方法

    用户使用:

    // 📁 用户文件
    
    import { addxx } from './a/add.js'
    import { updatexx } from './a/update.js'
    
    xxxx
    

    要这样用,会很麻烦!


实现:

文件目录:

📁 a
    index.js
    one.js
    two.js
    three.js
login.js
index.html  // 浏览器实现用的页面,没有关系

现实中是这样的,但是呢?我们是在浏览器上实现的!所以需要一个 html 页面 。

文件间的关系:

// 📁 one.js
function one(name) {
  return name + '+one 加工'
}

export { one }


// 📁 two.js
import { one } from './one.js'

let name = one('two 的名字')

function two () {
  console.log(name)
}

export { two }


// 📁 three.js
function three () {
  console.log('three的方法')
}

export { three }

然后我们在 index.js 中,把他们 导入 然后 导出:

// 📁 index.js

// 入口文件
export { two } from './two.js'
export { three } from './three.js'

用户在 login.js 文件中,想要使用文件中的方法:

// 📁 login.js

import { two, three } from './a/index.js'

two()
three()

最后,我们要在 html 页面上,导入用户文件,确保 模块的操作,在 浏览器 上能运行 。

// 📁 index.html
<body>
    <script type="module" src="login.js"></script>
</body>

在这里插入图片描述



需要对 默认导出 做处理:

我们的 three.js 改成 默认导出的:

// 📁 three.js
function three () {
  console.log('three的方法')
}

export default three

然后我们在 index.js 中导入和导出为:

export * from './three.js'
export { default } from './three.js'

总结:

  • 在声明前导出;
  • 声明 和 导出分开;
  • 导入全部 import * from xx.js
  • 起别名;
  • 默认导出 export default 和 命名导出;
  • 重新导出;

参考文章:https://zh.javascript.info/import-export#zhong-xin-dao-chu



动态导入 import()

概念

import(module) 表达式加载模块并返回一个 promise,返回的 promise 的 resolve 中包含所有导出的模块对象 。我们在 代码 中的任意位置都可以调用这个表达式 。

在页面中,导入模块时,使用 动态导入,不需要写 type=module 属性 。

结构:不确定,总之他会返回一个 promise

import('./xx.js')
.then((res) => { xxx })
.catch((err) => { xxx });

所以,我们可以使用 async / await 来简化接收 。



命名导出

// 📁 one.js
export function a() {
    console.log('aaaa')
}

export function b() {
    console.log('bbbb')
}

可以这么导入:

// 📁 xx.js
async function xxx() {
    let xx = await import('./one.js')
    
    xx.a()
    xx.b()
}

因为使用 async / await 所以把他放到了函数中;



默认导出:

// 📁 two.js
export default c () {
    console.log('cccc')
}

使用动态导入

// 📁 xx.js
async function xx() {
    let xx = await import ('./two.js')
    xx.befault()  // c()
}


具体实现

多个模块,我们在 html 页面中,导入他们当中的方法,然后在 页面 中点击按钮,运行模块中的方法 。

文件目录:

index.html
index.js
one.js
two.js
three.js

📁 one.js:

// 不向外提供的方法
function one(name) {
  return name + ' + one 加工'
}

export { one }

📁 two.js:

import { one } from './one.js'

let name = one('two 的名字')
function two () {
  console.log(name)
}

export { two}

📁 three.js:

function three () {
  console.log('three的方法')
}

export default three

📁 index.js:

export { two } from './two.js'

export * from './three.js'
export { default } from './three.js'

📁 index.html:

<body>
    <script>
        async function login() {
            let aa = await import('./index.js')
            aa.two()
            aa.default()
        }
    </script>

    <button onclick="login()">点击</button>
</body>

点击能出现对应的方法 。

参考文章: https://zh.javascript.info/modules-dynamic-imports

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值