【前端工程化】Node基础+模块化

140 阅读8分钟

Node概述

Node是一个内嵌了Chrome V8引擎的应用程序,以支持Javascript的运行,它由C++和C编写而成。具体来讲,用户编写的JS代码会经过V8进行解析,然后通过中间件(libuv)去调用相应的系统基础服务,比如文件读写,网络服务等。下载地址:Node.js — 下载 Node.js® (nodejs.org)

Node应用场景

前端应用

  1. 现在前端开发相关的库都是用node管理(将工具打包成node包并上传到对应仓库)
  2. npm、pnpm、yarn(使用这些node支持下的工具进行包的下载)
  3. 前端脚本撰写

后端应用

  1. web服务器
  2. SSR,即与服务端渲染有关

Node版本控制

使用如nvm-windows来进行node版本控制,下载地址:github.com/coreybutler…

nvm常用命令

  1. nvm install 下载指定版本Node
  2. nvm list 查看目前已有Node版本
  3. nvm use 切换到指定版本

Node的输入和输出

Node输入

node main1.js env=development

此时程序内部使用process.argv来接收参数,process是node内置的全局变量,argv是其中的参数数组。

Node输出

一般用console.log即可

Node中的全局变量

  1. global(类似于浏览器中的window,setTimeout,setInterval都会被挂载到上面,区别是在Node中用var定义变量后该变量不会被挂载到global上,但在浏览器中用var定义变量后该变量会被挂载到window上,为了统一命名,新的es语法中,在浏览器和Node端都使用了globalthis作为统一的标识符)

  2. process

  3. setTimeout()

  4. setInterval()

  5. 与时间循环机制相关(后面补充):

    1. nextTick()
    2. setImmediate()
  6. 与模块化相关(后面补充):

    1. require
    2. module
    3. exports

模块化

模块化的重要性

早期js开发是没有模块化的,如果在各个js文件中定义变量,然后再在html页面中进行各个js文件的引入,各个js文件中不同的变量/函数命名如果相同的话会造成冲突。

模块化的方案发展

ECMAScript直至ES6(2015)才推出了属于自家的标准模块化方案,在此之前,社区的模块化方案各种各样,AMD和CMD现在已经被淘汰,而CommonJS模块化方案直至今天仍然非常常用,Node采用的模块化方案就是CommonJS,而Webpack又是基于Node,因此也存在许多CommonJS的影子。

CommonJS

CommonJS制定了一套规范,比如规定了导出就使用exports关键字,导入使用require关键字。制定的规范由各个框架如Node和Webpack负责实现,注意,浏览器是没有实现CommonJS规范的。

Node中使用CommonJS:

Node中每一个js文件都规定为一个模块,每个模块都有自己的exports变量,另一个模块用require()获取该对象, 每个模块想导出变量用exports.env="development", 想导入变量用const utils=require("./module1.js") 这里导入的变量为了后续方便使用,可以用解构方式的写法: const {env}=require("./module1.js")

实际开发时候使用的导出方式

module.exports={env:development}

这里需要有前置知识:Node中每一个模块都是Module类的一个实例化,真正的导出实现是使module.exports=(一个内存中的空间),require()的时候也是取的是module.exports指向的内存空间。但是为了符合CommonJS的规范,Node中令exports=module.exports,于是才有了上个部分的exports导出方式,但是实际上我们都是使用module.exports=新对象进行导出,这样的写法明显很方便。至于,为什么不用exports=新对象进行导出,就是因为当exports=新对象时候,module.exports仍然指向原来的老对象,因此导入会失效。

require()函数细节

require()函数所能接收到的参数有:

  1. Node内置模块名称,如require("http")

  2. node_modules文件夹里面的第三方库名称,如require("axios"),实际查找到的是axios文件夹内的index.js,这里注意,如果当前模块没有node_modules,则会自动往父级目录查找是否具有node_modules。

  3. 相对/绝对路径

    如果使用相对/绝对路径,比如./utils,首先会找是否有名为utils的文件,若没有则自动为其拼接后缀.js或.json或.node,如果还是找不到,则为其拼接index文件名,如./utils/index,此时若找不到相应文件名又仍会自动为其拼接后缀并按.js->.json->.node的顺序进行查找。

require()函数导入模块时,被导入模块内部的代码会执行一次,如果进行多次导入,那么由于module的缓存机制,多次导入后被导入的模块内代码仍然只是执行一次。如果有循环导入,那么导入模块的方式是深度优先遍历

require()函数的缺点在于require()是同步的,不适用于浏览器端,因此浏览器端也没有相应的CommonJS的实现,因此早期的浏览器端的模块化用的是AMD/CMD,但是现在也不用了。

前端模块化的方案概述

  1. ES Module,前提是浏览器支持ES Module
  2. Webpack,使用模块化打包工具Webpack则没有浏览器限制,而且在Webpack中模块化方案可以使用CommonJS或ES Module或两者混用

ES Module

ES Module原理(了解)
  1. 构建

    浏览器在发现type="module"的script标签后知道该模块为ES模块,于是进行构建,首先为其创建一个模块记录,并下载该模块,同时如果该模块内部导入了其他模块,也同样进行相同的操作,在构建的过程中会形成对应的依赖关系,清晰各个模块间的依赖关系,防止多次重复下载。

  1. 实例化

    在解析到相应的import需要导入的变量时候形成相应的导入环境记录,解析到相应的export需要导出的变量时候形成相应的导出环境记录,并分配对应的内存空间。

  2. 求值

    在真正进行代码的执行时会为2中的环境记录进行赋值,使其成功取得模块内的变量,填充2中提到的内存空间。 下图为实例化+求值的过程。

ES Module写法
  1. export{}导出
//导出方式一
function getTime(){}
export{getTime} //这里export{}是一种特殊的语法,getTime在这里是一个标识符
//导出方式二
export function getTime(){}
//导出方式三(不常用)
function getTime(){}
export{getTime as fGetTime} //导出时起别名,as作为起别名的效果
  1. import{}导入
//导入方式一
import {getTime} from "./format.js" //注意这里可不是在Node里面,文件名不能省略.js
console.log(getTime())
//导入方式二
import {getTime as fGetTime} from "./format.js" //导入时起别名
console.log(fGetTime())
//导入方式三
import * as foo from "./format.js" //这里直接将foo作为整个模块进行导入。
console.log(foo.getTime())
  1. index.html的写法
<script src="./main.js" type="module"></script>
<!--注意一定要加type="module",浏览器才可以识别ESModule-->

在浏览器中直接使用ES Module时,需要开启一个服务进行使用,不能直接使用本地文件的形式打开index.html,因为浏览器因为安全性的需要仅支持一些特定协议,直接用本地文件形式打开的话使用的协议是file://协议,会有CORS错误。

ES Module的优化写法

一个包(比如工具包utils)内部总是有多个模块,那么此时适合在包内使用一个index.js,负责所有包内模块的导出,别的地方如果用到这个工具包则只需要从index.js导入即可,不需要关心某个函数具体是由包内哪个模块实现的。

//utils包里面的index.js
//基础写法
//formatDate,formatTime,parseLyric各来自utils包内各个模块
import {formatDate,formatTime} from "./format.js";
import {parseLyric} from "./parse.js";
export {
    formatTime,
    formatDate,
    parseLyric,
}
//优化写法1 推荐该写法,可读性与简洁性兼顾
export {formatDate,formatTime} from "./format.js"
export {parseLyric} from "./parse.js"
//优化写法2 该写法可读性差,在有文档的情况下考虑该写法
export * from "./format.js"
export * from "./parse.js"
//main.js,即使用到utils工具包里面的函数的一个模块
import {
    formatTime,
    formatDate,
    parseLyric
} from "./utils/main1.js"
default默认导出

default默认导出可以进一步屏蔽导入时候被导入模块的实现细节,功能:

  1. export default可以不用指定导出名
  2. 在导入时可以不用{},并且可以自己指定标识符
  3. 方便与CommonJS规范混用(不重要)
//format.js
export default function (){//不需要指定函数名
    return "2024.2.2"
}
​
//main.js
import getTime from "./format.js" //getTime是自定义标识符console.log(getTime())
import()函数

上面所述的import关键字仅能在js文件顶层使用,实际开发时候可能会碰到一些需要经过逻辑判断后才确定是否导入相应模块的情况,这时需用到import()函数。import()函数返回一个promise对象,使用.then()获取返回的结果。

//format.js
export function getTime(){
    return "2024.2.3"
}
//main.js
const flag=true
if(flag){
    const iPromise=import("./format.js")//import()返回一个promise对象
    iPromise.then(res=>{
        console.log(res.getTime())
    })
}
import.meta

import.meta是ES11(ES2020)新特性,记录了当前模块的一些信息,比如import.meta.url记录了模块的路径。