浏览器中的 ESM

简介: 早期的web应用非常简单,可以直接加载js的形式去实现。随着需求的越来越多,应用越做越大,需要模块化去管理项目中的js、css、图片等资源。

截屏2021-11-19 上午10.10.27.png

早期的web应用非常简单,可以直接加载js的形式去实现。随着需求的越来越多,应用越做越大,需要模块化去管理项目中的js、css、图片等资源。这里有很多大家熟悉的模块化标准, CJS、AMD、CMD、UMD 等等。模块化提供了我们更好的方式来组织和维护函数以及变量。而在 npm 生态开发的背景下,CJS 模块是开发过程中接触最多也是无法避免的。但由于浏览器并不能直接执行基于 CJS 打包的模块,因此类似 webpack 等打包工具便应运而生。随着webpack 大有一统构建工具的趋势下,JavaScript 官方的标准化模块系统ESM完成了。本文主要介绍下模块化标准间差异、基本加载原理。



模块化发展


1、为何要模块化


仔细想想,用 JavaScript 编码就是管理变量。这一切都是关于为变量赋值,或为变量添加数字,或将两个变量组合在一起并将它们放入另一个变量中。


截屏2021-11-19 上午10.10.41.png


如果代码中仅有少量的变量,那么组织起来其实是很简单的。一旦有很多的变量,我们会通过函数作用域去组织变量。因为函数作用域的缘故,一个函数无法访问另一个函数中定义的变量。


如果只维护少量变量非常简单。但是如果有很多的变量,我们就需要用一种方法来帮助做到这一点,叫做作用域。由于作用域在 JavaScript 中的工作方式,函数不能访问在其他函数中定义的变量。


截屏2021-11-19 上午10.10.50.png


这种方式是很有效的。在写一个函数的时候,只需要考虑当前函数,而不必担心其它函数可能会改变当前函数的变量。如果想要函数之间共享变量要怎么办呢?一种通用的做法是全局作用域。


在 jQuery 时代这种提升做法相当普遍。在我们加载任何 jQuery 插件之前,我们必须确保 jQuery 已经存在于全局作用域。


截屏2021-11-19 上午10.11.57.png


所有的 <script> 必须以正确的顺序排列,必须保证被依赖的变量先加载。如果排列错了,那么在运行过程中,应用将会抛出错误,并且停止继续运行。

代码之间的依赖是不透明的。这使得代码维护变得困难。代码变得充满不确定性。任何函数都可能依赖全局作用域中的任何变量。

其次,由于变量存在于全局作用域,所以任何代码都可以改变它。


2、模块化的作用


模块化为你提供了一种更好的方式来组织变量和函数。可以把相关的变量和函数放在一起组成一个模块。这种实现形式可以把函数和变量放在模块作用域中。模块作用域还提供一种暴露变量给其他模块使用的方式。模块可以明确地指定哪些变量、类或函数对外暴露。


对外暴露的过程称为导出。一旦导出,其他模块就可以明确地声称它们依赖这些导出的变量、类或者函数。


截屏2021-11-19 上午10.12.10.png


因为这是一种明确的关系,所以你可以很简单地辨别哪些代码能移除,哪些不能移除。


拥有了在模块之间导出和导入变量的能力之后,你就可以把代码分割成更小的、可以独立运行地代码块了。基于这些代码块,你就可以像搭乐高积木一样,创建所有不同类型的应用。比较流程的规范有CommonJS,AMD,CMD,ES,UMD等



3、现有模块标准


CJSCommonJS 的缩写。只适用于node端:

const _ = require('lodash'); 
module.exports = function doSomething(n) {
}

AMD 代表异步模块定义。在浏览器端有效:

define(['dep1', 'dep2'], function (dep1, dep2) {
    return function () {};
});

UMD 代表通用模块定义(Universal Module Definition):

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define([], factory);
    } else if (typeof module === 'object' && module.exports) {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory();
    } else {
        // Browser globals (root is window)
        root.returnExports = factory();
  }
}(typeof self !== 'undefined' ? self : this, function () {
    // Just return a value to define the module export.
    // This example returns an object, but the module
    // can return a function as the exported value.
    return {};
}));



什么是 ESM


简介


ESM是ES6提出的标准模块系统,ECMAScript mod

<script type="module">
  import { html, Component, render } from 'https://unpkg.com/htm/preact/standalone.module.js';
  class App extends Component {
    state = {
      count: 0
    }
    add = () => {
      this.setState({ count: this.state.count + 1 });
    }
    render() {
      return html`
        <div class="app">
          <div>count: ${this.state.count}</div>
          <button onClick=${this.add}>Add Todo</button>
        </div>
      `;
    }
  }
  render(html`<${App} page="All" />`, document.body);
</script>

思考:上述代码和在webpack中开发有啥区别?


2、浏览器端技术实现


回顾下Webpack执行流程

  1. 本地模块化解析(通过webpack或者babel,将import解析成cjs)
  2. 将各个库打包成一个js boundle
  3. 开启服务,托管资源
  4. 浏览器获取资源
  5. 执行代码


浏览器端ESM执行流程

  1. 开启服务,托管资源(ES源码)
  2. 加载入口文件,浏览器模块化解析截屏2021-11-19 上午10.14.29.png
  3. 构建
  1. 遍历依赖树,先解析文件,然后找出依赖,最后又定位并加载这些依赖,如此往复。(下载所有的js)
    截屏2021-11-19 上午10.14.42.png
  2. 模块映射
    当加载器要从一个 URL 加载文件时,它会把 URL 记录到模块映射中,并把它标记为正在下载的文件。然后它会发出这个文件请求并继续开始获取下一个文件。
    截屏2021-11-19 上午10.14.53.png
  3. 解析模块
    所有的模块都按照严格模式来解析的。不同文件类型按照不同的解析方式称。在浏览器中,通过
    type="module" 属性告诉浏览器这个文件需要被解析为一个模块。不过在 Node 中,我们并不使用 HTML 标签,所以也没办法通过 type 属性来辨别。社区提出一种解决办法是使用 .mjs 拓展名。
  1. 运行
    采用深度优先的后序遍历方式,顺着关系图到达最底端没有任何依赖的模块,然后设置它们的导出。模块映射会以 URL 为索引来缓存模块,以确保每个模块只有一个模块记录。这保证了每个模块只会运行一次。



3、为什么火起来


  • ES语法基本确定
  • http2普及
  • 新浏览器普及
  • 开发与发布代码一致
  • 启动快
  • 全新加载模式


目前浏览器支持:


截屏2021-11-19 上午10.16.31.png截屏2021-11-19 上午10.16.50.png


目前只有5%的浏览器不兼容es相关规范。



4、为什么还没火起来


  • 部分浏览器的兼容性
  • 历史包袱悠久
  • 生态不完善



实战



当我们在项目中使用需要考虑以下几个问题点

1. 代码开发需要基于es开发

let a = 1;
new Promise()
() => {}
...

2. 依赖库加载

  • node_modules代码服务化
  • 兼容cjs
  1. 加载包内部es目录
  2. cjstoesm


14.png


  • CDN(network for npm)
  1. https://unpkg.com/
  2. https://www.skypack.dev/


3. 兼容不支持的浏览器

  • type="module"实现
  • 如果浏览器不支持,他只识别type="text/javascript"不识别 type="module" ,故不下载js;如果支持,则会下载js
  • 如果浏览器不支持,则会忽略nomodule,下载js;如果支持,则不会下载js
<script type="module" src="app.js"></script>
<script nomodule src="app-bundle.js"></script>
<script src="system.js"></script>
<script type="systemjs-importmap">
{
  "imports": {
    "lodash": "https://unpkg.com/lodash@4.17.10/lodash.js"
  }
}
</script>
<script type="systemjs-module" src="/js/main.js"></script>


4. jsx支持

  • 通过其他开源库
<script type="module">
    import { html, Component, render } from 'https://unpkg.com/htm/preact/standalone.module.js';
    class App extends Component {
      render() {
        return html`
          <div class="app">
          </div>
        `;
      }
    }
    render(html`<${App} page="All" />`, document.body);
</script>
  • 本地语法糖解析
<APP {...Props}/>
=>
React.createElement(App, {...props})


现有脚手架


1. snowpack

截屏2021-11-19 上午10.18.16.png

  • 托管node_modules
  • 支持图片、css等资源
  • JSX 和 Typescript 编译
  • HMR
  • ...

2. vite

https://cn.vitejs.dev/guide/

目前snowpack的作者后续可能不再维护了,所以推荐大家使用vite



ESM 未来



2018 年 5 月 Firefox 60 发布后,所有的主流浏览器就都默认支持 ESM 了。Node 也正在添加 ESM 支持,为此还成立了工作小组来专门研究 CJS 和 ESM 之间的兼容性问题。所以,在未来你可以直接在 <script> 标签中使用 type="module",并且在代码中使用 importexport 。同时,更多的模块功能也正在研究中。比如动态导入提案已经处于 Stage 3 状态;import.meta也被提出以便 Node.js 对 ESM 的支持;模块定位提案 也致力于解决浏览器和 Node.js 之间的差异。


相信在不久的未来,跟模块一起玩耍将会变成一件更加愉快的事!


node v10以上版本全部支持ESM https://kentcdodds.com/blog/super-simple-start-to-es-modules-in-node-js



相关参考



ECMAScript modules in browsers https://jakearchibald.com/2017/es-modules-in-browsers/

JavaScript 模块现状 https://zhuanlan.zhihu.com/p/26567790

基于esm、html、unpkg的前端开发模式:https://github.com/developit/htm

How I Build JavaScript Apps In 2021:https://timdaub.github.io/2021/01/16/web-principles/

Find out how much turning on modern JS could save. https://estimator.dev/

什么是amd、commonjs、umd、esm? https://zhuanlan.zhihu.com/p/96718777

ES modules: A cartoon deep-dive:https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/

import.map:https://github.com/WICG/import-maps

面对 ESM 的开发模式,webpack 还有还手之力吗? https://topic.atatech.org/articles/202736



相关文章
|
Java
百度搜索:蓝易云【hutool Http 工具发送POST请求的几种方式。】
以上是使用Hutool发送POST请求的几种方式。根据实际需求和代码复杂度,选择合适的方式来发送POST请求。
1246 0
|
2月前
|
人工智能 固态存储 Linux
用 Codex 的朋友,真的建议你看一眼硬盘写入
Codex CLI 存在日志滥用问题:其 `logs_2.sqlite-wal` 文件持续高频写入 TRACE 级日志(含流式报文、IO 遥测等),实测21天写入37TB,或致消费级SSD提前报废。WAL文件删除后空间不释放,需先终止进程。建议用户立即检查并清理日志。
1311 1
|
缓存 JavaScript 前端开发
拿下奇怪的前端报错(三):npm install卡住了一个钟- 从原理搞定安装的全链路问题
本文详细分析了 `npm install` 过程中可能出现的卡顿问题及解决方法,包括网络问题、Node.js 版本不兼容、缓存问题、权限问题、包冲突、过时的 npm 版本、系统资源不足和脚本问题等,并提供了相应的解决策略。同时,还介绍了开启全部日志、使用替代工具和使用 Docker 提供 Node 环境等其他处理方法。
16726 2
|
Oracle 关系型数据库 数据库
【赵渝强老师】在PostgreSQL中访问Oracle
本文介绍了如何在PostgreSQL中使用oracle_fdw扩展访问Oracle数据库数据。首先需从Oracle官网下载三个Instance Client安装包并解压,设置Oracle环境变量。接着从GitHub下载oracle_fdw扩展,配置pg_config环境变量后编译安装。之后启动PostgreSQL服务器,在数据库中创建oracle_fdw扩展及外部数据库服务,建立用户映射。最后通过创建外部表实现对Oracle数据的访问。文末附有具体操作步骤与示例代码。
1456 6
【赵渝强老师】在PostgreSQL中访问Oracle
|
JavaScript 前端开发 Go
async vs defer 的区别
async vs defer 的区别
386 0
|
缓存 Ubuntu Linux
Linux中yum、rpm、apt-get、wget的区别,yum、rpm、apt-get常用命令,CentOS、Ubuntu中安装wget
通过本文,我们详细了解了 `yum`、`rpm`、`apt-get`和 `wget`的区别、常用命令以及在CentOS和Ubuntu中安装 `wget`的方法。`yum`和 `apt-get`是高层次的包管理器,分别用于RPM系和Debian系发行版,能够自动解决依赖问题;而 `rpm`是低层次的包管理工具,适合处理单个包;`wget`则是一个功能强大的下载工具,适用于各种下载任务。在实际使用中,根据系统类型和任务需求选择合适的工具,可以大大提高工作效率和系统管理的便利性。
1841 25
|
前端开发 JavaScript
原生实现环形进度条
原生实现环形进度条
729 121
|
存储 缓存 数据可视化
HarmonyOS5云服务技术分享--云存储指南
本文详解HarmonyOS云存储实战技巧,涵盖文件上传、下载、元数据操作及删除等核心功能。通过简单易懂的示例代码,助你快速上手。云存储支持自动同步、精细权限管理与海量存储,适合处理用户头像、游戏存档等场景。文中还提供避坑指南、进阶技巧和最佳实践,帮助开发者高效利用云存储功能,减少开发障碍。附完整代码示例,欢迎交流!
|
移动开发 小程序 JavaScript
微信小程序学习实录6(百度经纬度采集、手动调整精度、H5嵌入小程序、百度地图jsAPI、实时定位、H5更新自动刷新)
微信小程序学习实录6(百度经纬度采集、手动调整精度、H5嵌入小程序、百度地图jsAPI、实时定位、H5更新自动刷新)
708 1
echarts中使用散点scatter更改颜色却不生效的问题
本文讨论了在ECharts中使用散点图(scatter)时更改颜色不生效的问题。原因是项目中使用了`visualMap`组件,它具有最高的优先级,导致自定义的颜色设置被覆盖。解决方法是在`visualMap`组件中增加`seriesIndex`属性,指定它只对特定的系列(series)生效,从而避免影响散点图的颜色设置。文章提供了详细的代码示例和解决办法。
650 3