模块化的发展
早期的模块化不是真正的模块化,只是通过一些“骚操作”实现看似是模块化的效果,例如立即调用函数表达式(简称IIFE)就是一个在定义时可立即执行的函数,至于它如何实现模块化,可查看MDN文档,在此不深入讲述了。后期的模块化才算是真正的模块化,它包括CJS、AMD、CMD、UMD和ESM,经过多年演变,目前Web开发倾向于ESM,Node开发倾向于CJS。
模块化的核心包括以下特性,基本都是围绕如何处理文件(模块)。
- 拆分:将代码根据功能拆分为多个可复用模块
- 加载:通过指定方式加载模块并执行与输出模块
- 注入:将一个模块的输出注入到另一个模块
- 管理:因为工程模块数量众多需管理模块间的依赖关系
模块化的作用
模块方案
使用多个<script>
的问题:
- 请求过多:每个
<script>
都有一个src必然会增加HTTP请求次数 - 依赖模糊:每个
<script>
的摆放顺序都有可能影响前后脚本加载错误 - 难以维护:每个
<script>
的变量命名与函数作用域都有可能互相影响
JS模块化
六种常见模块方案,分别是IIFE、CJS、AMD、CMD、UMD和ESM。
每个模块方案的特性
同步加载包括IIFE与CJS,异步加载包括AMD、CMD和ESM。浏览器可兼容IIFE与AMD,服务器可兼容CJS,浏览器与服务器都兼容CMD、UMD和ESM。
CJSvsESM
- 运行时加载指整体加载模块生成一个对象,再从对象中获取所需的属性方法去加载。最大特性是全部加载,只有运行时才能得到该对象,无法在编译时做静态优化。
- 编译时加载指直接从模块中获取所需的属性方法去加载。最大特性是按需加载,在编译时就完成模块加载,效率比其他方案高,无法引用模块本身(本身不是对象),但可拓展JS高级语法(宏与类型校验)。
现状:ESM能否在Node环境中运行
原生支持ESM
v8.9.0
命令中加上—experimental-modules,Node就可象征性地支持ESM。
v13.2.0
默认支持ESM。
—experimental-modules特性包括以下方面。
-
使用type指定模块方案
-
在package.json中指定type为commonjs,则使用CJS
-
在package.json中指定type为module,则使用ESM
-
使用—input-type指定入口文件的模块方案,与type一样
-
命令中加上—input-type=commonjs,则使用CJS
-
命令中加上—input-type=module,则使用ESM
-
支持新文件后缀.cjs
-
文件后缀使用.cjs,则使用CJS
-
使用—es-module-specifier-resolution指定文件名称引用方式
-
命令中加上—es-module-specifier-resolution=explicit,则引用模块时必须使用文件后缀(默认)
-
命令中加上—es-module-specifier-resolution=node,则引用模块时无需使用文件后缀
-
使用main根据type指定模块方案加载文件
-
在package.json中指定mian后会根据type指定模块方案加载文件
CJS/ESM判断方式
mjs文件使用ESM解析,cjs文件使用CJS解析,js文件使用基于package.json指定的type解析(type=commonjs使用CJS,type=module使用ESM)。
Node会将以下情况视为ESM。
- 文件后缀为.mjs
- 文件后缀为.js且在package.json中指定type为module
- 命令中加上—input-type=module
- 命令中加上—eval cmd
方案:部署Node的ESM开发环境
将Node v13.2.0作为高低版本分界线,当版本>=13.2.0则定为高版本,当版本<13.2.0则定为低版本。高版本使用Node原生部署方案,低版本使用Node编译部署方案。
在根目录中创建package.json并执行npm i安装项目依赖。
创建src/index.js文件,加入以下内容。示例引用我开源的@yangzw/bruce-us,其中NodeType()用于获取Node相关信息。
Node原生部署方案
添加模块类型,并指定Node/Npm版本限
为了让Node支持ESM,还需为其指定Node/Npm版本限制。这是为了避免预设与实际情况不同而报错,例如预设该项目在高版本运行,实际却在低版本运行。
Node与Npm是成双成对地安装,可通过Node Releases查询到Node v13.2.0对应Npm v6.13.1。
解决显示文件名称
在命令中加上—es-module-specifier-resolution=node就能解决显示文件名称的问题。
解决特性无法使用问题
- __filename与__dirname可用import.meta对象重建
- require、module和exports可用import与export代替
- json文件的引用可用Fs模块的readFileSync与JSON.parse()代替
ESM模块不会导出导入值而是引用值。
- 导入引用模块可访问该引用但无法修改它。
- 导出引用模块可为引用该模块的模块重新分配值且该值由导入引用模块使用
Node编译部署方案
用babel将代码从ESM转换为CJS
babel的四个核心子包:
- @babel/cli:提供支持@babel/core的命令运行环境
- @babel/core:提供转译函数
- @babel/node:提供支持ESM的命令运行环境
- @babel/preset-env:提供预设语法转换集成环境
将start命令中的node替换为babel-node。
兼容更低版本Node,可在package.json中指定babel的targets。
监听脚本自动重启命令
nodemon是一个自动检测项目文件发生变化就重启服务的Npm模块。
执行npm i -D nodemon安装nodemon,在package.json中指定nodemonConfig相关配置,将start命令替换为nodemon -x babel-node src/index.js。
nodemon配置可查看Nodemon官网。
扩展阅读
https://juejin.cn/book/7034689774719860739/section/7034911744845676548