模块化
概念:
一个模块就是 一个文件,一个脚本 。模块间可以相互加载,使用 import
和 export
关键之来进行 导入 和 导出 。
-
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页面加载,从上往下执行,
- 执行第 2 个 输出语句;
- 加载到 按钮标签;
- 执行第 3 个输出语句;
- 最后,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
到我们想要使用的文件中不就好了吗?
咳咳!是啊,但在某个场景下,这种做法很有用哦!
背景:
-
假设 a 文件夹,里面是有很多文件,整个文件夹 都是 一些封装好的方法,我们可以去调用里面的方法,来实现某些功能;
a文件夹: index.js // 入口文件,我们要实现的 add.js // 实现数据累加的方法 update.js // 实现数据修改的方法 remove.js // 实现数据删除的方法 user.js ......
-
里面的方法们各种都是 互相有联系的,就会相互导入和导出方法;
// 📁 update.js function updatexx () { xxxxxx } export { updataxx } // 📁 add.js import { updatexx } from './update.js' // add 中的一些方法 function addxx () { xxx } export { addxx }
-
整个文件夹里,有一些是对外提供的方法,有一些是内部实现的方法,不应该被用户调用。
-
我们就可以创建一个文件,当做这个 封装好的方法(整个文件)的 入口文件,在这个文件上 导出我们对用户提供的方法 。
// 📁 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'
不然要这个导入
addxx
和updatexx
这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