浅谈前端模块化

简介: 前沿:有玩过qiankun的童鞋都知道,若想保证qiankun父应用能够成功正常加载子应用。我们需要“包装好”子应用,在官方文档中可以看到对子应用的打包方式中,就是通过将webpack的输出模式定位为 UMD 。你可以能好奇,这个UMD是个什么玩意?

微信截图_20220514155114.png


前沿:有玩过qiankun的童鞋都知道,若想保证qiankun父应用能够成功正常加载子应用。我们需要“包装好”子应用,在官方文档中可以看到对子应用的打包方式中,就是通过将webpack的输出模式定位为 UMD 。你可以能好奇,这个UMD是个什么玩意?


微信截图_20220514155123.png


1. UMD


UMD 叫做通用模块定义规范,也是前端模块化演变出的一种模块化定义,是模块定义的跨平台解决方案。它支持运行时让同一个代码的模块,在使用 Commonjs、AMD等其他模块化规范项目中运行,换句话说,UMD可以让你的代码兼容基于其他多种模块化规范写的模块,统一浏览器端以及非浏览器端的模块化方案的规范,通用性很强,但本质上他没有自己的“规范”,他其实就是个集合,将CommonJs、AMD等规范汇聚于一体,就可以同时支持import、require和script直接引用,简单理解可以看下图👇


微信截图_20220514155133.png


1.1 使用场景


假设你现在有一个场景:你需要开发一个工具库,一个util的方法,与此同时你想让这个代码(开发的类库)既能在nodejs环境直接使用,又能在浏览器中使用,那么我们就使用umd的输出模式,只需在 webpackoutput 中添加libraryTarget: 'UMD' 即可,就可以更好地解决跨平台、跨环境等问题,有兴趣的同学可以看看之前写的工具库 kdutil,就是基于umd模式下打包


1.2 qiankun为什么需要将子应用输出为umd


qiankun架构下的子应用通过 webpack 的 umd 输出格式来做,让父应用在执行子应用的 js 资源时可以通过 eval,将 window 绑定到一个 Proxy 对象上,以此来防止污染全局变量,方便对脚本的 window 相关操作做劫持处理,达到子应用之间的脚本隔离。


1.3 运行机制 🚗


通过webpack配置libraryTarget:umd来配置如何暴露 library,将你的library暴露为所有的模块定义下都可运行的方式,平时开发中童鞋应该很少配置webpack的output.libraryTarget的配置,一般在开发插件的场景中使用得比较多。常见的配置除了umd还有:var、this、window、global、commonjs、commonjs2、amd、amd-require等,不过umd兼容性最好 umd方式输出配置如下


// webpack.config.js
module.exports = {
 output: {
    filename: `kdutil.min.js`,
    path: path.resolve(rootPath, 'dist'),
    library: `kdutil`,
    libraryTarget: "umd"
  },
 }
 // index.js
export default {
  name: 'hello 🌲酱',
};


微信截图_20220514155142.png


  • 先判断是否支持CommonJS2规范(exports是否存在以及module是否存在),存在则使用的是CommonJS2方式加载。


  • 再判断是否支持AMD(define是否存在,因为AMD规范是通过define来定义模块的),存在则使用AMD方式加载模块,我们看下下面这段webpack输出的结果


  • 其次判断是否支持CommonJS规范(exports是否存在),存在则使用的是CommonJS方式加载模块。


  • 前两个都不存在,则将模块公开到全局(window 或 global);


我们尝试直接以<script>的引用方式引入kdutil这个全局访问的变量名,通过直接引用这个用umd输出的模块,在浏览器直接访问全局的kdutil,发现输出的内容跟我们导出的不一样,如下所示


<script src="../dist/kdutil.min.js"></script>
 <script type="text/javascript">
  console.log(kdutil);
</script>


微信截图_20220514155154.png


👨‍🎓 啊轩同学:怎么多一层default,这样的话使用变量还需要以kdutil.default这样的写法,有没有办法简洁些?


答::使用export default导出的全局变量会多一个default属性,可以在webpack.out输出添加一个配置:libraryExport: "default"来解决


👨‍🎓 啊乐同学:CommonJS是Node.js的模块规范,那上面那个CommonJS2又是什么鬼?


答:其实本质上就是在区别输出,使用的是exports还是module.exports,我们都知道CommonJS规范定义了exports才是亲生的,而module.exports顶多算是个“养子”,CommonJS2规范是规定用module.exports来输出的


我们借助webpack分别通过这两种规范来打包来对比就一目了然


微信截图_20220514155900.png


区别在于,如果定义的是CommonJS需要定义一个output.library的名称,可以用来exports导出使用,反而如果使用CommonJS2,你则可以不用定义output.library,直接导出使用


🌲 拓展阅读:


2.前端模块化


👨‍🎓 啊呆同学:那你说umd是CommonJS、AMD 的结合,那CommonJS、CMD、AMD这几个分别又有什么区分呢?


答:前端模块化是指,通过将前端代码根据一定的规则解耦封装成几个代码文件(模块),并对外暴露特定的接口或方法,然后在项目开发中根据具体情况进化合理的组合的方法,本质上有助于开发效率的提升、提高代码复用率、方便依赖关系管理。


2.1 前端模块化的演变


回顾前端模块化的发展,从早期的简单函数封装、对象封装、到立即执行函数表达式(IIFE)、script标签按js依赖执行顺序加载等的简单模块化使用,再到后面形成模块化规范演变,如下图所示


微信截图_20220514155813.png


2.2 模块化规范的区分


  • CommonJS: Nodejs采用的Commonjs(也叫cjs)这个规范,但因为Nodejs加载模块是同步的。一个模块的加载需要加载完成后(同步加载),才能执行后面的操作。我们知道服务器加载模块文件一般都已经放在服务器自身的硬盘,加载速度快。使用方式是通过exports或modules.exports导出,require引入


  • AMD : 全称叫异步模块定义,与前者Commonjs不同的是,它是异步加载的,因为浏览器需要加载,且模块都放在服务器端,有等待时间,需要请求下来,导致浏览器会有一段"假死"的状态。于是就有个AMD规范诞生,让浏览器端的模块支持异步加载,使用方式是通过define方法定义模块(将所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行), 需要依赖RequireJS


  • CMD : 与AMD一样,都是通过异步加载,不同的是,AMD是提前执行,而CMD是延迟执行,且CMD推崇的是依赖就近加载,AMD 推崇的是依赖前置加载, 需要依赖seajs


  • ES6模块化:自从后来出了ES6模块化,AMD及CMD基本不用了,而是用ES6中自带的模块化,但是部分浏览器还不支持ES6语法,则需要借助Babel来“翻译”,对于Babel翻译可以看之前树酱的Babel傻傻看不懂,使用方式可以使用export default命令为模块指定默认输出及用import导入


运行环境 加载方式 依赖第三方库 是否需要babel编译
CommonJS 服务器 同步加载 不需要
AMD 浏览器 异步加载 requireJs
CMD 浏览器 异步加载(按需加载) seajs
UMD 浏览器 + 服务器 异步加载 不需要
ES6 Module(esm) 浏览器 + 服务器 同步加载 不需要


🌲 拓展阅读:


2.3 ES6 Module 和 CommonJs的区别


👨‍ 啊雪同学: CommonJs的用法和ES6 Module的用法好容易混淆,两者有什么区分吗?


答:有,看下面这个区分表格


CommonJS ES6 Module
输出方式 输出的是一个值的拷贝 输出的是值的引用,模块可以实时变化
运行 运行时加载 编译时输出接口
是否支持 treeshake 不支持 支持(因为支持静态分析)


关于输出方式,前者是拷贝值,后者是引用值


微信截图_20220514155900.png


前者当lib.js模块加载后,它的内部变化就影响不到输出的lib.counter了,因为本质上lib.counter是一个原始类型的值,会被缓存。而后者ES6 模块的运行机制与 前者CommonJS 不一样。当JS引擎对模块进行静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值,不会被缓存


本质上是因为前者对模块依赖的解决是动态的(模块依赖关系的建立是发生在代码运行阶段),而后者是静态的(模块依赖关系的建立是发生在代码编译阶段)


🌲 拓展阅读:


2.4 import能替代require,进行动态加载吗?


微信截图_20220514155910.png


我们先看上面这段代码,有没有错。上一节讲过,import在js引擎处理时,是在发生编译的阶段。而这个阶段是不会去分析或执行这个if语句,所以import语句放在if中是没有任何意义的,会报语法错误,也就是说import和export命令只能在模块的顶层。


就好比我要加载一个动态的模块,用require我可以这样写。也就是说require此时加载的是哪一个模块,我们只有等到运行的时候才知道


微信截图_20220514155923.png


啊凌同学:那除了require就没有其他方式进行动态加载模块了吗?


答:有,ES2020提案引入了import()函数,可以用来支持动态加载模块,两者区别在于,前者require是同步加载,后者是异步加载,具体用法请看使用文档 🔗


微信截图_20220514155934.png


2.5 如何在Node环境使用ES6模块加载


啊斌同学:树酱,那我如果想在node环境使用es6模块加载,可以吗?


答:可以,但是不推荐混用。node从v13.2 版本开始就默认打开了ES6 module支持。如果想在node环境使用es6模块加载,则需要.mjs后缀文件名。也就是Node.js 遇到.mjs文件,则会认为它是ES6模块。如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。但这个时候也就意味着,该目录下js只能解释ES6 模块,如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs。


简单说: .mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置



相关文章
|
6月前
|
资源调度 前端开发 JavaScript
构建高效前端项目:现代包管理器与模块化的深度解析
【2月更文挑战第21天】 在当今快速演变的前端开发领域,高效的项目管理和代码组织已成为成功交付复杂Web应用的关键。本文将深入探讨现代前端包管理器如npm, yarn和pnpm的工作原理,以及它们如何与模块化编程实践(例如CommonJS、ES6模块)协同工作以优化开发流程。我们将剖析这些工具的内部机制,了解它们如何解决依赖冲突,提高安装速度,并保证项目的健壮性。同时,本文还将介绍模块化编程的最佳实践,包括代码拆分、重用和版本控制,帮助开发者构建可维护且性能卓越的前端项目。
|
6月前
|
前端开发 JavaScript jenkins
构建高效前端项目:从模块化到自动化
【2月更文挑战第13天】 随着Web技术的不断进步,前端项目的复杂性日益增加。为了确保可维护性和性能,前端工程师必须采用模块化和自动化的策略来优化开发流程。本文将探讨如何使用现代前端工具和最佳实践来构建一个高效的前端项目架构,包括模块打包、代码分割和持续集成等方面。
|
18天前
|
JavaScript 前端开发 Java
VUE学习四:前端模块化,ES6和ES5如何实现模块化
这篇文章介绍了前端模块化的概念,以及如何在ES6和ES5中实现模块化,包括ES6模块化的基本用法、默认导出与混合导出、重命名export和import,以及ES6之前如何通过函数闭包和CommonJS规范实现模块化。
49 0
VUE学习四:前端模块化,ES6和ES5如何实现模块化
|
21天前
|
前端开发 JavaScript 开发者
深入解析前端开发中的模块化与组件化实践
【10月更文挑战第5天】深入解析前端开发中的模块化与组件化实践
21 1
|
3月前
|
前端开发 JavaScript
前端必会的JavaScript模块化
【8月更文挑战第3天】JavaScript模块化
26 1
|
5月前
|
JavaScript 前端开发
必知的技术知识:esm前端模块化
必知的技术知识:esm前端模块化
98 0
|
6月前
|
前端开发 JavaScript 安全
【Web 前端】怎么实现Module模块化?
【5月更文挑战第1天】【Web 前端】怎么实现Module模块化?
|
6月前
|
JavaScript 算法 前端开发
【专栏】前端开发中的slot算法和shadow DOM,两者提供更灵活、高效和模块化的开发方式
【4月更文挑战第29天】本文探讨了前端开发中的slot算法和shadow DOM,两者提供更灵活、高效和模块化的开发方式。slot算法允许在组件中定义插槽位置,实现内容的灵活插入和复用,提高代码可读性和维护性。shadow DOM则通过封装DOM子树,实现样式和事件的隔离,增强组件独立性和安全性。这两种技术常应用于组件开发、页面布局和主题定制,但也面临兼容性、学习曲线和性能优化等挑战。理解并掌握它们能提升开发效率和用户体验。
99 2
|
6月前
|
前端开发 JavaScript 安全
前端模块化发展
前端模块化发展
|
6月前
|
JavaScript 前端开发 开发者
【Web 前端】JS模块化有哪些?
【4月更文挑战第22天】【Web 前端】JS模块化有哪些?