入门JavaScript(二):模块管理
JS文件通常被引入HTML中发挥作用。例如一个简单的弹窗,可以在 index.js
中这样写
function foo() {
alert('This is foo!');
}
然后在 index.html
中导入
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
</head>
<body>
<button id="foo-btn" onclick="foo()">foo</button>
<script src="index.js"></script>
</body>
</html>
在项目逐渐复杂后,我们需要在 index.js
中导入其他 .js
文件的函数/对象,也就是将其他 .js
文件作为模块来加载。
JS历史中模块管理的范式有很多,比如CommonJS范式,关键字是 require
;ES6范式,关键字是 export, import
。目前逐渐成为主流的是ES6范式
ES6导入
使用ES6模块,有几个点需要注意:
- 需要通过
<script type='module' src='index.js'></script>
,显式声明.js
是ES6模块。而类似index.js
这样,应用程序加载的第一个.js
文件,一般被称为入口文件(entry point)。常见的命名包括index.js, main.js, app.js
等。 - 模块需要使用
export, import
显式导出、导入。未导出的对象始终不可见 - ES6模块的设计原则是避免命名空间污染。在以上案例
index.js
导入后,其内部定义的所有对象(如函数foo()
)都会暴露为全局变量,这有可能造成意外的变量相互覆盖。但同时,这也允许我们在<button>
标签中直接引用foo()
。使用ES6模块后,foo()
将变得不可见,我们需要在模块中用addEventListener
实现等效的功能 - ES6模块导入受到CORS的限制,这是一种避免计算机被恶意代码入侵的机制。在本地文件浏览器中双击
index.html
,我们的代码将无法像以上案例一样完美运行。需要开启本地的HTTP服务
创建 js/mod1.js
,我们写下一个函数
export function foo1() {
alert('This is foo1!');
}
然后将该函数在 index.js
中引用。同时,需要使用 addEventListener
为两个按钮添加处理函数
import { foo1 } from './js/mod1.js';
export function foo() {
alert('This is foo!');
}
document.addEventListener('DOMContentLoaded', (event) => {
document.querySelector('button#foo-btn').addEventListener('click', foo);
document.querySelector('button#foo1-btn').addEventListener('click', foo1);
});
在 index.html
中,我们需要以 type="module"
的形式引入 index.js
,而且不能再用 onclick
绑定事件了
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
</head>
<body>
<button id="foo-btn">foo</button>
<button id="foo1-btn">foo1</button>
<script type="module" src="index.js"></script>
</body>
</html>
写完代码之后,为了避免CORS,我们还需要启动本地HTTP服务器访问 index.html
。Vscode用户可以使用插件 Live Server
开启本地HTTP服务
如上图,开启的HTTP服务端口号是5500,路径是 /module/index.html
,这是因为我把 index.html
放在 module
目录下。index.html
总是被省略
ES6模块除了可以导入函数外,还可导入对象。例如在 js/mod2.js
中
export const obj1 = {
name: 'John',
age: 20
};
export const obj2 = {
name: 'Jane',
age: 22
};
然后在 index.js
中引用
import { obj1, obj2 } from './js/mod2.js';
console.log(obj1)
console.log(obj2)
ES6模块的 export, import
还有更多种用法,具体参考这里
第三方模块管理
以上我们讨论了自定义模块的用法。在实际开发中,使用、管理别人开发好的模块非常关键
JS模块一般有两种导入方式:在线导入、离线导入。在线导入是JavaScript很常见,但在其他语言中很少见的方式,像这样:
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
如此我们就导入了托管在 unpkg
上的 React
模块。其他常见的托管网站有 jsdelivr, cdnjs
等
有很多在线模块不支持ES6导入,所以大多时候,我们就如此将模块内对象全部暴露到全局环境
离线导入是其他语言中常见的模块管理方式,JS常用npm/yarn/pnpm管理离线包,类似Python的pip。其中npm是nodejs默认的包管理器,而pnpm效率更高、越发流行
我们可以使用如下命令,全局安装pnpm
npm install -g pnpm
JS的离线包管理有个特点,会把所有依赖下载到项目 node_modules
目录下,并将具体版本、依赖关系,存储在 package.json
和 pnpm-lock.yaml
中。这是为了保证项目环境的独立性、完整性
如果需要像其他语言一样全局装包,需要指定 -g
参数,但不应该频繁这样做
# 将包添加到packages.json中
pnpm add pkg
# 添加指定版本
pnpm add pkg@3.2.1
# 将packages.json指定的包安装到node_modules目录下
pnpm i
# 安装pkg,并将其添加到packages.json
pnpm install pkg --save
# 从package.json、node_modules删除包
pnpm remove pkg
# 安装某包到全局
pnpm add -g pkg
安装后,即可使用ES6语法导入模块。nodejs会默认检索 node_modules
,不需要写完整路径
import React from 'react'
nodejs版本管理
有时,某些版本的第三方包只能在特定版本的nodejs上运行,这就要求我们管理多个nodejs环境,就像Python的Conda
主流的管理工具是 n
(是的,它就叫 n
)和 nvm
,前者是一个node模块,后者是一个bash脚本。nvm
更加严谨、干净,操作也稍复杂,对于大多数情况,n
提供的功能完全够用
# 全局安装n
npm install -g n
# 查看node版本
node -v
# 安装最新版本的nodejs
n latest
# 安装nodejs v17
n 17
在多个nodejs存在时,命令行输入 n
,回车,即可切换特定版本
node/8.17.0
node/18.14.0
ο node/18.14.2
node/20.6.0
Use up/down arrow keys to select a version, return key to install, d to delete, q to quit
结语
以上,我们讨论了JS模块管理和nodejs版本管理的方法。此时你应该想见识一下好用的第三方模块,去这里探索一番吧
玩得开心!