入门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模块,有几个点需要注意:

  1. 需要通过 <script type='module' src='index.js'></script>,显式声明 .js是ES6模块。而类似 index.js这样,应用程序加载的第一个 .js文件,一般被称为入口文件(entry point)。常见的命名包括 index.js, main.js, app.js等。
  2. 模块需要使用 export, import显式导出、导入。未导出的对象始终不可见
  3. ES6模块的设计原则是避免命名空间污染。在以上案例 index.js导入后,其内部定义的所有对象(如函数 foo())都会暴露为全局变量,这有可能造成意外的变量相互覆盖。但同时,这也允许我们在 <button>标签中直接引用 foo()。使用ES6模块后,foo()将变得不可见,我们需要在模块中用 addEventListener实现等效的功能
  4. 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服务

image.png

如上图,开启的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.jsonpnpm-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版本管理的方法。此时你应该想见识一下好用的第三方模块,去这里探索一番吧

玩得开心!