模块化到打包构建引入

1,003 阅读11分钟

由于项目组内需要完成每年一次的KPI,所以做了一个与模块化相关的知识分享会,想想既然都再组内分享了,那就再在掘金上写一遍。

image.png

开整

模块化前因

发展史 - 1

JavaScript语言设计的初始目标,是作为浏览器的脚本语言,仅仅用于实现一些页面效果 那个时候,一个页面所用到的 JS 可能只有区区几百行 在这种情况下,语言本身的一些缺陷往往容易被忽略,因为使用的不多,规模太小,也没有专门的人来写前端

大事件:

  • 1996年,NetScape将JavaScript语言提交给欧洲的一个标准制定阻止ECMA(欧洲计算机制造商协会)
  • 1998年,NetScape在与微软浏览器IE的竞争中失利,宣布破产

发展史 - 2

AJAX的出现,逐渐改变了浏览器脚本语言的使用,使得JavaScript不仅可以实现页面上的一些效果,还可以与服务端进行数据交互。

所以这个时候,前端的代码量就愈发增加,后端工程师压力剧增,使得一些公司开始招聘专门的前端开发者

但这个时候前端也同时还存在一些问题:

  • 浏览器解释执行 JS 的速度太慢
  • 用户端的电脑配置不足
  • 更多的代码带来的全局污染,依赖关系混乱

大事件:

  1. IE浏览器制霸市场后,几乎不再更新
  2. ES4.0流产,导致JS语言10年间几乎毫无变化
  3. 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年出炉 ,

至此前端开启了百花齐放的大时代

image.png

CommonJs

在nodejs中,由于有且仅有一个入口文件(启动文件),而开发一个应用肯定会涉及到多个文件配合,因此,nodejs对模块化的需求比浏览器端要大的多

由于nodejs刚刚发布的时候,前端没有统一的、官方的模块化规范,因此,它选择使用社区提供的CommonJS作为模块化规范

在学习CommonJS之前,首先认识两个重要的概念:模块的导出模块的导入

导出

要理解模块的导出,首先要理解模块的含义

什么是模块?

模块就是一个JS文件,它实现了一部分功能,并隐藏自己的内部实现,同时提供了一些接口供其他模块使用

模块有两个核心要素:隐藏暴露

隐藏的,是自己内部的实现

暴露的,是希望外部使用的接口

任何一个正常的模块化标准,都应该默认隐藏模块中的所有实现,而通过一些语法或api调用来暴露接口

暴露接口的过程即模块的导出

导入

当需要使用一个模块时,使用的是该模块暴露的部分(导出的部分),隐藏的部分是永远无法使用的。

当通过某种语法或api去使用一个模块时,这个过程叫做模块的导入

规范

CommonJs规定使用 require 导入和 exports 导出

具体规范如下

  1. 如果一个JS文件中存在exportsrequire,该JS文件是一个模块
  2. 模块内的所有代码均为隐藏代码,包括全局变量、全局函数,这些全局的内容均不应该对全局变量造成任何污染
  3. 如果一个模块需要暴露一些API提供给外部使用,需要通过exports导出,exports是一个空的对象,你可以为该对象添加任何需要导出的内容
  4. 如果一个模块需要导入其他模块,通过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对模块做出了以下处理

  1. 为了保证高效的执行,仅加载必要的模块。nodejs只有执行到require函数时才会加载并执行模块

  2. 为了隐藏模块中的代码,nodejs执行模块时,会将模块中的所有代码放置到一个函数中执行,以保证不污染全局变量。

     (function(){
         //模块中的代码
     })()
    
  3. 为了保证顺利的导出模块内容,nodejs做了以下处理

    1. 在模块开始执行前,初始化一个值module.exports = {}
    2. module.exports即模块的导出值
    3. 为了方便开发者便捷的导出,nodejs在初始化完module.exports后,又声明了一个变量exports = module.exports
     (function(module){
         module.exports = {};
         var exports = module.exports;
         //模块中的代码
         return module.exports;
     })()
    
  4. 为了避免反复加载同一个模块,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 会做以下两件事

  1. 通过模块路径查找模块
  2. 将文件中的代码放入一个函数中执行,并将执行后的结果缓存起来,并返回

可以认为,nodejs 执行模块时,是同步的

所以想把 commomjs 模块导入浏览器显然不太现实,因为浏览器需要同步去请求模块(因为线上环境 ,模块一般都放在远程服务器上)

但是不能说,浏览器不能实现模块化,只需解决同步问题即可:

  1. 远程同步加载 js 费事,那就改成异步加载
  2. 需要放到函数中执行(避免变量污染),那就在编写模块时候直接包一个函数

AMD

AMD全称 Asynchronous Module Definition,即异步模块加载机制

Require.js 实现了 AMD规范

在 AMD 中,导入和导出的代码,都必须放置在 define 函数中

image.png

CMD

CMD全称 Common Module Definition,公共模块定义规范

sea.js 实现了 CMD规范

在 CMD 中,导入和导出的代码,都必须放置在 define 函数中

image.png

UMD

UMD同时兼容AMD和CommonJS

  1. CommonJS模块是根据服务器第一原则开发的。选择同步加载,其模块不需要包装。
  2. UMD判断是否存在支持Node.js的模块(exports),然后使用Node.js模块模式。在判断是否支持AMD(define是否存在)时,使用AMD加载模块

image.png

ES模块化

ECMA组织参考了众多社区模块化标准,终于在2015年,随着ES6发布了官方的模块化标准,后成为ES6模块化

ES6模块化具有以下的特点

  1. 使用依赖预声明的方式导入模块
    1. 依赖延迟声明
      1. 优点:某些时候可以提高效率
      2. 缺点:无法在一开始确定模块依赖关系(比较模糊)
    2. 依赖预声明
      1. 优点:在一开始可以确定模块依赖关系
      2. 缺点:某些时候效率较低
  2. 灵活的多种导入导出方式
  3. 规范的路径表示法:所有路径必须以./或../开头

image.png

基本导出

export 声明表达式

export { 具名符号 }

基本导入

/**
 * 由于使用的是**依赖预加载**,因此,导入任何其他模块,导入代码必须放置到所有代码之前
 * 
 *  注意以下细节:

  - 导入时,可以通过关键字```as```对导入的符号进行重命名
  - 导入时使用的符号是常量,不可修改
  - 可以使用*号导入所有的基本导出,形成一个对象
 */

import { 导入的符号列表 } from '模块路径';

// 默认导出
/**
 * 每个模块,除了允许有多个基本导出之外,还允许有一个默认导出
   默认导出类似于 CommonJS 中的module.exports,由于只有一个,因此无需具名
 * 
 */

export default 默认导出的数据 // 或者 
export { 默认导出的数据 as default };

默认导入

import 接收变量名 from '模块路径';
import 接收默认导出的变量, { 接收基本导出的变量 } from '模块路径'; // 默认导入和基本导出并存

其他细节

import '模块路径'; // 可以使用无绑定的导入用于执行一些初始化代码
export { 绑定的标识符 } from '模块路径'; // 可以使用绑定再导出,来重新导出来自另一个模块的内容

webpack配置打包文件类型

针对webpack项目,如果想打包成指定的格式,需要配置其 librarylibraryTarget 属性

image.png


module.exports = {
    output: {
        library: 'commonjs', // 设置打包的模式,可以配置字符串,也可以配置成对象
        libraryTarget: 'xx库名' // 设置打包后的库名
    }
}

以下是针对ice框架组件打包配置和生成配置文件分析

配置打包示例

配置打包内容为 commonjs

image.png

打包文件分析

image.png

可以看到有很明显的 Commonjs标识 exports, 由此可知,此配置是可以修改打包内容格式的

vite配置打包文件类型

由于vite在项目打包时,使用的是rollup,所以具体配置需要查询rollup配置信息

image.png

可以看到,rollup主要是通过 format配置,修改其打包文件类型的

配置打包示例

接下来具体实操看看,譬如我想打包成 esModule 模块

image.png

打包文件分析,看到入口是esModule的

image.png

如果把umd和esm格式的文件作为外部资源引用

引入配置案例是使用vite,webpack配置也是大致类似的。

umd格式

对于我们项目打包优化的常见做法之一,就是把一些不变资源采用cdn引入,所以引入就涉及到如何在我们项目中使用

此刻我用 nprogress 模块举例,可以看到目前我再static文件下放着一份 umd 格式的 nprogress 模块

image.png

如果想要在项目中使用,我们需要在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 属性,但是该属性可能兼容不了低版本的浏览器

image.png

如果能保证是运行在新的环境下,是可以使用 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

image.png

打包后 可以直接 安装 server 包,直接运行下,看是否可以运行成功。

结语

以上就是此文章的完整内容,谢谢观看