如何在Node.js里使用ES6 import?

随着Node.js V16 LTS版本的发布,终于对ES6的支持不用再加–experimental-modules参数来启用了。不过,对前端开发来说,在Node.js里使用ES6的import会显得有些不太一样。

我们先看看现在是什么样子的。

ES6 import介绍

import 语句用于导入由其他模块export出的模块(module)。模块是包含一段可重用代码的文件。无论是否声明,导入模块都处于严格模式(strict mode)。

语法示例:

import name from 'module-name'

可以通过多种方式进行导入:

1. 导入整个模块:

import * as name from 'module-name';

2. 从模块导入默认导出:

import name from 'module-name';

3. 从模块导入单个导出:

import { name } from 'module-name';

4. 从一个模块导入多个导出:

import { nameOne , nameTwo } from 'module-name';

5. 仅导入用于副作用的模块

import './module-name';

Node.js 不支持直接 ES6 import。如果我们尝试使用import导入别的文件,就会抛出错误。SyntaxError: Cannot use import statement outside a module

启用ES6 Import

拿下面这段代码来举例:

import express from 'express';
  
const app = express();
  
app.get('/',(req,res) => {
    res.send('乐码范');
})
  
const PORT = 5000;
  
app.listen(PORT,() => {
    console.log(`Running on PORT ${PORT}`);
})

在默认情况下运行,会报这样的错:

(node:18268) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
import express from 'express';
^^^^^^
SyntaxError: Cannot use import statement outside a module
    at Object.compileFunction (node:vm:352:18)
    at wrapSafe (node:internal/modules/cjs/loader:1031:15)
    at Module._compile (node:internal/modules/cjs/loader:1065:27)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:17:47

使用Node.js V16+版本的话,启用ES6 import的支持很简单,正如上面错误中的提示那样,只需要在package.json里添加下面第二行:"type" : "module"

{
    "name": 'my-package-name',
    "type" : "module"
}

那么上面的代码就可以正常启动了。是不是很简单?

意外的部分

对于熟悉用WebpackBrowserify, Rollup等代码打包工具的前端开发来说,import和require在一个代码文件里混用,只要配置好打包工具,完全不是个事。但是,对Node.js呢?却不是这样。

package.json里添加了"type" : "module"之后,下面的代码在运行node index.js的时候,就会报错:

const express = require('express');
  
const app = express();
  
app.get('/',(req,res) => {
    res.send('乐码范');
})
  
const PORT = 5000;
  
app.listen(PORT,() => {
    console.log(`Running on PORT ${PORT}`);
})

const express = require('express');
                ^
ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and 'C:\my-package\package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
    at file:///C:/my-package/index.js:3:17
    at ModuleJob.run (node:internal/modules/esm/module_job:185:25)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:281:24)
    at async loadESM (node:internal/process/esm_loader:88:5)
    at async handleMainPromise (node:internal/modules/run_main:65:12)

从错误信息来看,在不做代码改动的情况下,我们是没法再用require的。在上面的例子中,我们可以把index.js文件改成index.cjs,然后通过node index.cjs命令运行起来。

不过,如果是在有大量现有的代码文件基础上改,并且是改的入口文件的话,就需要把所有的引用到的代码的后缀都改成.cjs,这个不仅繁琐,而且得不偿失。

所以,不在做特殊hack的情况下,Node.js里的CommonJS module只能require CommonJS的module;ES6 module能import ES6的module和CommonJS的module,但是需要CommonJS module的文件后缀都是.cjs才可以。比如:import('./server.cjs')

对于有大量文件的Node.js项目,改成支持ES6 module的项目,如果不做范围比较大的文件后缀或代码改动的话,其实还是比较难迁移到ES6 module的。ES6 module比较适合新的项目,一开始就用import的话,代码就会显得一致且没有迁移的成本。

本文只是简单探讨了一下ES6 module在Node.js里的简单的使用,更多的技术细节请参考:Node.js官方文档