由于项目组内需要完成每年一次的KPI,所以做了一个与模块化相关的知识分享会,想想既然都再组内分享了,那就再在掘金上写一遍。
开整
模块化前因
发展史 - 1
JavaScript语言设计的初始目标,是作为浏览器的脚本语言,仅仅用于实现一些页面效果 那个时候,一个页面所用到的 JS 可能只有区区几百行 在这种情况下,语言本身的一些缺陷往往容易被忽略,因为使用的不多,规模太小,也没有专门的人来写前端
大事件:
- 1996年,NetScape将JavaScript语言提交给欧洲的一个标准制定阻止ECMA(欧洲计算机制造商协会)
- 1998年,NetScape在与微软浏览器IE的竞争中失利,宣布破产
发展史 - 2
AJAX的出现,逐渐改变了浏览器脚本语言的使用,使得JavaScript不仅可以实现页面上的一些效果,还可以与服务端进行数据交互。
所以这个时候,前端的代码量就愈发增加,后端工程师压力剧增,使得一些公司开始招聘专门的前端开发者
但这个时候前端也同时还存在一些问题:
- 浏览器解释执行 JS 的速度太慢
- 用户端的电脑配置不足
- 更多的代码带来的全局污染,依赖关系混乱
大事件:
- IE浏览器制霸市场后,几乎不再更新
- ES4.0流产,导致JS语言10年间几乎毫无变化
- 2008年ES5回归,仅解决了一些 JS API 不足的糟糕局面
发展史 - 3
2008年,谷歌的 V8 引擎发布,将JS的执行速度推上了一个新的台阶
至此制约前端发展的问题大部分都得以解决,除了全局变量污染和依赖混乱的问题
恰逢此刻,有一个叫 Ryan Dahl 的小伙子,想开发一个高性能的web服务,于是他就基于开源的 V8 引擎,开发出一套服务端程序,这个程序被命名为 NodeJs
为了处理剩下的两个问题,通过社区的激烈讨论,也形成了一个模块化方法,就是 commonjs(同步执行),此方案一出,也立刻被 NodeJs 所支持。
大事件:
- 2008年,V8发布
- IE的市场逐步被 firefox 和 chrome 蚕食,现已无力回天
- 2009年,nodejs发布,并附带commonjs模块化标准
发展史 - 4
但由于commomjs是同步执行,所以运行到浏览器存在一些问题,浏览器无法异步加载,如果同步请求,会导致页面阻塞。
所以 叕(zhuo 第二声 ) 经过社区讨论,AMD&CMD规范出炉了,它两解决的问题和commonjs一样,但是可以更好的适应浏览器环境。
经过这一系列的讨论,ESMA官方开始考虑 JS 的模块化问题,所以 ES模块化在2015年出炉 ,
至此前端开启了百花齐放的大时代
CommonJs
在nodejs中,由于有且仅有一个入口文件(启动文件),而开发一个应用肯定会涉及到多个文件配合,因此,nodejs对模块化的需求比浏览器端要大的多
由于nodejs刚刚发布的时候,前端没有统一的、官方的模块化规范,因此,它选择使用社区提供的CommonJS作为模块化规范
在学习CommonJS之前,首先认识两个重要的概念:模块的导出和模块的导入
导出
要理解模块的导出,首先要理解模块的含义
什么是模块?
模块就是一个JS文件,它实现了一部分功能,并隐藏自己的内部实现,同时提供了一些接口供其他模块使用
模块有两个核心要素:隐藏和暴露
隐藏的,是自己内部的实现
暴露的,是希望外部使用的接口
任何一个正常的模块化标准,都应该默认隐藏模块中的所有实现,而通过一些语法或api调用来暴露接口
暴露接口的过程即模块的导出
导入
当需要使用一个模块时,使用的是该模块暴露的部分(导出的部分),隐藏的部分是永远无法使用的。
当通过某种语法或api去使用一个模块时,这个过程叫做模块的导入
规范
CommonJs规定使用 require 导入和 exports 导出
具体规范如下
- 如果一个JS文件中存在
exports或require,该JS文件是一个模块 - 模块内的所有代码均为隐藏代码,包括全局变量、全局函数,这些全局的内容均不应该对全局变量造成任何污染
- 如果一个模块需要暴露一些API提供给外部使用,需要通过
exports导出,exports是一个空的对象,你可以为该对象添加任何需要导出的内容 - 如果一个模块需要导入其他模块,通过
require实现,require是一个函数,传入模块的路径即可返回该模块导出的整个内容
// utils.js
var a = 1;
var b = 2;
exports.c = 123;
// 当重新改变module.exports的指向时,exports配置的属性失效
module.exports = {
a,
b,
}
// index.js
var utils = require('./utils');
if (true) {
// 可以写在条件里,同步执行
var utils1 = require('./utils');
}
node中对Commonjs的实现
为了实现CommonJS规范,nodejs对模块做出了以下处理
-
为了保证高效的执行,仅加载必要的模块。nodejs只有执行到
require函数时才会加载并执行模块 -
为了隐藏模块中的代码,nodejs执行模块时,会将模块中的所有代码放置到一个函数中执行,以保证不污染全局变量。
(function(){ //模块中的代码 })() -
为了保证顺利的导出模块内容,nodejs做了以下处理
- 在模块开始执行前,初始化一个值
module.exports = {} module.exports即模块的导出值- 为了方便开发者便捷的导出,nodejs在初始化完
module.exports后,又声明了一个变量exports = module.exports
(function(module){ module.exports = {}; var exports = module.exports; //模块中的代码 return module.exports; })() - 在模块开始执行前,初始化一个值
-
为了避免反复加载同一个模块,nodejs默认开启了模块缓存,如果加载的模块已经被加载过了,则会自动使用之前的导出结果
// 完整伪代码
const cacheModule = {};
function myRequire(filePath) {
const path = require('path');
const fs = require('fs');
const realPath = path.resolve(__dirname, filePath);
const fileContent = fs.readFileSync(realPath, 'utf-8');
// 实现模块缓存
if (cacheModule[realPath]) {
return cacheModule[realPath];
}
const module = {
exports: {},
};
return (function (module) {
var exports = module.exports;
if (fileContent) {
eval(fileContent);
}
cacheModule[realPath] = module.exports;
return module.exports;
})(module);
}
AMD & CMD & UMD
因为使用 require 导入模块时,node 会做以下两件事
- 通过模块路径查找模块
- 将文件中的代码放入一个函数中执行,并将执行后的结果缓存起来,并返回
可以认为,nodejs 执行模块时,是同步的
所以想把 commomjs 模块导入浏览器显然不太现实,因为浏览器需要同步去请求模块(因为线上环境,模块一般都放在远程服务器上)
但是不能说,浏览器不能实现模块化,只需解决同步问题即可:
- 远程同步加载 js 费事,那就改成异步加载
- 需要放到函数中执行(避免变量污染),那就在编写模块时候直接包一个函数
AMD
AMD全称 Asynchronous Module Definition,即异步模块加载机制
Require.js 实现了 AMD规范
在 AMD 中,导入和导出的代码,都必须放置在 define 函数中
CMD
CMD全称 Common Module Definition,公共模块定义规范
sea.js 实现了 CMD规范
在 CMD 中,导入和导出的代码,都必须放置在 define 函数中
UMD
UMD同时兼容AMD和CommonJS
- CommonJS模块是根据服务器第一原则开发的。选择同步加载,其模块不需要包装。
- UMD判断是否存在支持Node.js的模块(exports),然后使用Node.js模块模式。在判断是否支持AMD(define是否存在)时,使用AMD加载模块
ES模块化
ECMA组织参考了众多社区模块化标准,终于在2015年,随着ES6发布了官方的模块化标准,后成为ES6模块化
ES6模块化具有以下的特点
- 使用依赖预声明的方式导入模块
- 依赖延迟声明
- 优点:某些时候可以提高效率
- 缺点:无法在一开始确定模块依赖关系(比较模糊)
- 依赖预声明
- 优点:在一开始可以确定模块依赖关系
- 缺点:某些时候效率较低
- 依赖延迟声明
- 灵活的多种导入导出方式
- 规范的路径表示法:所有路径必须以./或../开头
基本导出
export 声明表达式
export { 具名符号 }
基本导入
/**
* 由于使用的是**依赖预加载**,因此,导入任何其他模块,导入代码必须放置到所有代码之前
*
* 注意以下细节:
- 导入时,可以通过关键字```as```对导入的符号进行重命名
- 导入时使用的符号是常量,不可修改
- 可以使用*号导入所有的基本导出,形成一个对象
*/
import { 导入的符号列表 } from '模块路径';
// 默认导出
/**
* 每个模块,除了允许有多个基本导出之外,还允许有一个默认导出
默认导出类似于 CommonJS 中的module.exports,由于只有一个,因此无需具名
*
*/
export default 默认导出的数据 // 或者
export { 默认导出的数据 as default };
默认导入
import 接收变量名 from '模块路径';
import 接收默认导出的变量, { 接收基本导出的变量 } from '模块路径'; // 默认导入和基本导出并存
其他细节
import '模块路径'; // 可以使用无绑定的导入用于执行一些初始化代码
export { 绑定的标识符 } from '模块路径'; // 可以使用绑定再导出,来重新导出来自另一个模块的内容
webpack配置打包文件类型
针对webpack项目,如果想打包成指定的格式,需要配置其 library 和 libraryTarget 属性
module.exports = {
output: {
library: 'commonjs', // 设置打包的模式,可以配置字符串,也可以配置成对象
libraryTarget: 'xx库名' // 设置打包后的库名
}
}
以下是针对ice框架组件打包配置和生成配置文件分析
配置打包示例
配置打包内容为 commonjs
打包文件分析
可以看到有很明显的 Commonjs标识 exports, 由此可知,此配置是可以修改打包内容格式的
vite配置打包文件类型
由于vite在项目打包时,使用的是rollup,所以具体配置需要查询rollup配置信息
可以看到,rollup主要是通过 format配置,修改其打包文件类型的
配置打包示例
接下来具体实操看看,譬如我想打包成 esModule 模块
打包文件分析,看到入口是esModule的
如果把umd和esm格式的文件作为外部资源引用
引入配置案例是使用vite,webpack配置也是大致类似的。
umd格式
对于我们项目打包优化的常见做法之一,就是把一些不变资源采用cdn引入,所以引入就涉及到如何在我们项目中使用
此刻我用 nprogress 模块举例,可以看到目前我再static文件下放着一份 umd 格式的 nprogress 模块
如果想要在项目中使用,我们需要在index.html通过script进行引入,引入后,它会在全局声明一个 NProgress 这个属性,这样我们后续就可以通过这个变量使用到其内部的方法
有了变量,我们就得想办法,让其模块在打包时,不会打包到我们最终的产物去,在这里我使用的是vite-plugin-externals这个插件,
vite-plugin-externals可以帮助你指定哪些依赖应该被当作外部依赖处理,这样做可以减少最终构建文件的大小,并且通过在 HTML 文件中正确引入外部依赖,可以提高页面加载速度。
此时 我们只需要使用这个插件,即可,具体配置如下
import { viteExternalsPlugin } from 'vite-plugin-externals';
export default defineConfig({
plugins: [
viteExternalsPlugin({
nprogress: 'NProgress' // 项目内引入的属性: 全局的变量
})
]
})
esModule格式
由于 esModule 模块比较特殊,在html中,需要配合 type="module" 才可正确引入
所以当想要使用 esModule 模块时,需要配合 importmap 属性,但是该属性可能兼容不了低版本的浏览器
如果能保证是运行在新的环境下,是可以使用 esModule 作为外部资源,以下是具体配置
// 首先需要现在 index.html 中增加如下配置
<script type="importmap">
{ "imports": {
"vue": "/static/vue.js",
"@vue/devtools-api": "https://unpkg.com/@vue/devtools-api@6.4.5/lib/esm/index.js",
"vue-router": "/static/vueRouter.js",
"vue-demi": "/static/vueDemi.js",
"pinia": "/static/pinia.js",
} }
</script>
其次在修改打包格式为esm
打包后 可以直接 安装 server 包,直接运行下,看是否可以运行成功。
结语
以上就是此文章的完整内容,谢谢观看