webpack加载css-面对ESM开发模式,webpack还有反击之力吗?

Snowpack/vite等基于ESM的重构工具的出现,使得项目创建不再需要创建完整的bundle。 很多人认为我们不再需要包装工具的时代正式到来了。 借助浏览器的ESM能力,一些代码基本上可以直接运行,无需构建。 对于webpack来说,社区发起的这一波ESM热潮,将webpack编译的速度推向了风口浪尖。 在v5版本中,webpack在编译性能上也做了很多努力。 除了提供数学缓存优化之外,它还提供了Module Federation的解决方案,这给我们底层的应用实践带来了很大的想象空间。

以前webpack有统一重构工具的倾向,但现在结合业务的特点我们可以有更多的选择。

为什么需要打包

在JavaScript编程过程中,很多时候我们都在改变变量。 在一个复杂的项目开发过程中,如何管理函数和变量范围就显得尤为重要。 JavaScript的模块化为我们提供了更好的形式来组织和维护函数和变量。 众所周知的JavaScript模块不仅有上面提到的ESM,还有CJS、AMD、CMD、UMD等规范。 在npm生态开发的背景下,CJS模块是开发过程中暴露最多、最难避免的。 但由于浏览器无法直接执行基于CJS打包的模块,因此webpack等打包工具应运而生。

对于早期的Web应用来说,打包模块不仅可以处理JS模块化,还可以打包多个模块,组合网络请求。 使用这种构建工具来打包项目确实是一个不错的选择。 如今,基本上所有主流浏览器版本都支持ESM,并发网络请求带来的性能问题在HTTP/2的普及下已经不是以前那样了,所以大家都把目光转向了ESM。 就目前的体验来看,开发过程中基于原生ESM的建立率显然远远优于webpack等打包工具。

初步了解 ESM 构建工具

使用ESM

<script src="index.js" type="module"></script>


通过 type="module" 告诉浏览器当前脚本使用 ESM 模式,浏览器将构建依赖图,并利用浏览器原生的 ESM 能力完成模块搜索、分析、实例化和执行的过程。

为什么要快

为什么基于ESM的构建工具snowpack/vite在构建时比webpack快很多webpack加载css,借用snowpack官网的一张图片来说明:

两大核心特征:

首先,它们的创建复杂度很低。 修改任意组件,只需要进行单文件编译,时间复杂度始终为O(1)

有了ESM的能力,模块化就交给了浏览器端,不存在重复加载资源的问题。 如果不涉及jsx或typescript语法,甚至可以不编译直接运行

webpack加载css_加载中的图片_加载中

构建过程

如果只是将源代码交给浏览器执行,是无法满足大多数项目的需求的。 一般我们都是直接在源码中导入第三方模块。 此外,我们还会导出样式和资源,包括源代码开发过程中使用的最新es语法。 、jsx、ts语法,这些语法都不能在浏览器中直接运行。

施工工具根据不同的类型和要求来解决上述问题。 以下是流程概述的简化版本:

导入句子处理

对于esm模块,首先要处理的就是import语句,在项目开发过程中常见以下情况:

导入依赖项

在snowpack中,三方依赖语句被转换为/web_modules/*.js

import React from 'react';
// 转化为
import React from '/web_modules/react.js';

如果npm下的所有依赖仍然可以输出标准的ESM模块,那么这一步的处理可以很简单,只需拦截所有web_modules模块请求并返回node_modules下的ESM模块即可。 但实际情况是,npm 生态中的很多依赖尚未得到支持,所以目前的做法大多会在项目刚启动时进行预打包,完成 CJS/UMD 转换为 ESM 的操作。

配置逻辑通常还支持主动筛选已生成ESM的模块,降低预封装成本

导入图像

开发过程中导入图片资源时,其实是希望返回其静态资源的URI。 Snowpack首先重写此类资源的导入语句:

import img from './img.png';
// 转化为
import img from './img.png.proxy.js';


webpack加载css_加载中的图片_加载中

img.png.proxy.js文件中,默认导入对应的文件地址即可:

// img.png.proxy.js
export default '/dist/assets/img.png';


该工具生成*.proxy.js时,可以将资源复制到指定的输出路径。

导入CSS

样式导出的重写规则与图片类似,唯一的区别是生成*.css.proxy.js中的内容。

这个想法是将导出的 css 转换为 JS 模块。 对于css来说,除了通过link的形式导入之外,还可以通过style标签的方式注入。 css模块的代理规则也相当明确:

// code 便是 css 文件中读取的内容const code = ${JSON.stringify(code)};const styleEl = document.createElement("style");const codeEl = document.createTextNode(code);styleEl.type = 'text/css';styleEl.appendChild(codeEl);document.head.appendChild(styleEl);

如果要启动css模块,则snowpack中约定*.module.css文件来启用CSS Module功能。 与上述插入标签的形式相比,增加了样式类名对应的类名的导入,即:

...// let json = ${JSON.stringify(moduleJson)};let json = { "test": "App--Test--3kX9Z4E"}export default json;


除了上面提到的重写的导入类型之外,还有json、less、sass等文件。 处理逻辑本质上是类似的,都是通过代理导入到新生成的文件中,并在其中添加特定的脚本来完成最终的处理。 ESM 模块的改造以支持浏览器加载处理。

模块联盟的应用

ESM给我们带来的体验是很快的,因为它除了像webpack一样处理编译之外,不需要分析源码中的node_module依赖关系,并对它们进行合并、拆分、打包。

官方和社区都不遗余力地设计了各种方案来优化性能,比如cache-loader、thread-loader、dllPlugin、babel cacheDirectory、hard-source-webpack-plugin等优化方法,但实际效果并不理想没有那么震撼,而且有一定的使用成本。

直到webpack 5的出现,其带来的长期缓存能力可以在测量文件变化时根据依赖关系只编译依赖树上的相关文件,并且优化后的构建缓存和解析器缓存都得到了很大的提升。 速度。 另一个特性Module Federation(以下简称MF)的出现,带来了一种基于webpack开发的新的协作形式,使得不同构建任务之间的模块复用变得更加容易。

MF的设计动机是让不同的团队协作开发一个或多个应用程序。 这种辅助模式与今年如火如荼的微后端开发模式如出一辙。

MF方案可以拆分一个应用,导入不同的模块,并且模块所依赖的底层三方库也可以共享。 有了这个能力,我们可以尽早为应用程序建立共同的依赖关系,以减少编译内容和打包体积。

核心用法

首先我们来了解一下MF中的两个核心概念:

有了MF的能力,每个应用程序都可以引用其他应用程序,也可以被其他应用程序使用,实现单向共享

指示:

// webpack
const { ModuleFederationPlugin } = require("webpack").container;...plugins: [ new ModuleFederationPlugin({ name: 'remoteRuntime', // 必须,唯一 ID,作为输出的模块名,使用的时通过 ${name}/${expose} 的方式使用 remotes: ['remote'], // 可选,表示作为 Host 时,去消费哪些 Remote exposes: { // 可选,表示作为 Remote 时,export 哪些属性被消费 './ComponentA': './src/components/A', }, shared: ['react', 'react-dom'], // 可选,优先用 Host 的依赖,如果 Host 没有,再用自己的 })]


当consumer模块的应用程序使用exposes时,除了引入Host生成的remoteEntry(模块依赖)脚本外,代码级别还需要做相应的改变:

// 消费形式 import ${name}/${expose}import ComponentA from 'remoteRuntime/ComponentA';

执行逻辑

如何使用webpack 5模块联邦的能力,除了工程配置之外,源码层面也需要做出相应的改变:

// index.jsimport('./bootstrap');
// bootstrap.jsimport React from 'react';import ReactDOM from 'react-dom';import ComponentA from 'ComponentA';...


在执行渲染逻辑之前,需要降低一层bootstrap的导入逻辑。 本质是让异步加载的运行时依赖起来。 加载完成后,执行主要逻辑。 核心流程如下:

代码核心执行流程变化如下:

首先加载index.js,它通常是应用程序的主包

它将被加载到主包中,__webpack_require__(“./boostrap.js”)

boostrap模块将包含依赖于chunk的各种信息

所有应用依赖加载完毕后,执行最终的应用逻辑

依赖关系提取模式

明白了上面的原理之后,我们回头看看webpack项目是否可以像ESM模式那样不打包依赖,预先规划好所有三方依赖,在主应用启动时直接依赖这种早期编译模块。

借助MF的能力,所有三方依赖都可以作为远程依赖引入。 在ICE的实践中,使用MF方案以及webpack 5的数学缓存确实给项目开发速度带来了很大的提升。 该方案的核心实现逻辑如下:

首先通过exposes能力,导入所有项目运行时依赖,完成remoteEntry及相关远程模块的建立

该项目实现了MF能力并依赖于已建立的远程

通过babel能力将项目中的运行时依赖转换为远程模块加载方式webpack加载css,即import xx from {expose}的方式

上述处理完成后,项目启动后的bundle将不再将远程打包的运行时相关依赖打包在一起。

优化前:

webpack加载css_加载中的图片_加载中

启用场景优化后:

以上解决方案已在不同类型的业务中实践,欢迎您联系我们讲解使用场景

同类方案对比

动态链接库

应用程序的三方依赖被编译成dll。 每次应用程序依赖项发生变化时,都需要重新构建应用程序,并且很难按需加载。 在多个应用程序之间共享dll基本上是困难的,并且缺乏动态特性。

外部因素

与外部解决方案相对应,依赖项被创建为单独的文件。 当应用程序打包时,需要声明引用了哪些外部模块。 外部无法按需加载,相应的依赖和脚本加载顺序必须自己维护。

概括

去年,snowpack/vite的工程生态和完善的定制能力与webpack有很大不同。 随着国外对ESM生态的重视,越来越多的重构工具开始尝试ESM方法。 发展。

ESM的开发模式很大程度上解决了dev开发中的启动速度问题。 针对很多模块没有导入ESM的情况,还提供了预编译的形式。 同时,snowpack等工具在生产构建模式下提供基于webpack打包的插件,让开发者无需太多负担即可应用最终产品。 这确实是一种渐进的方法,但肯定不是一个长期的解决方案。

webpack的慢不仅需要分析依赖并打包成bundle,而且使用babel编译和sass-loader能力也需要很长时间。 这也是为什么当今社区主流的ESM模块方案在编译源码时都选择esbuild作为默认的编译工具,而且它的编译速度足够快。 webpack还利用优化化学缓存的方法来提高构建的性能,尤其是二次构建和热更新还可以大大提高编译率。

webpack提供的能力更像是一套企业级的解决方案。 它为源代码/构建的任何节点提供了足够的钩子和能力,方便开发人员进行定制。 另一方面,ESM依赖于浏览器的模块加载能力,并且不解决模块依赖关系。 内部实现逻辑以速度为首要考虑,能交给浏览器直接运行的就不会被编译。 对于ICE或者社区的一些应用开发框架,为了增加开发者的认知和开发成本,大多数方案都会结合工程和运行时能力来简化实现中的开发成本,逻辑处理对开发者的认知和开发成本有一定的影响。 webpack 的生态。 依赖程度。 那么如何将这部分能力与ESM开发模式结合起来,帮助开发者无论是项目创建体验还是源码开发体验都得到极大的提升,将是另一个重点加码的风口。

关注“阿里巴巴F2E”微信公众号(左)微信视频号(右)

把握阿里巴巴后台新动向