前端工程化浅谈和模块化方案深耕🔥🔥

2,667 阅读13分钟

前言

前端工程化一直是一个很热门的话题,一直都很受大家关注。
尤其近几年前端的发展可谓是日新月异,虽是后起之秀但野蛮生长成为具有极具影响力的行业。然而整个前端生态的飞速发展离不开的一个话题就是前端工程化
具体怎么定义前端工程化,我在网上也查找了一些资料,无外乎都是一些宏大又宽泛的概念。
而最近正好在解读Vue的源码,又涉及到前端工程化这个概念,所以我想着按自己的理解写一篇概述,希望自己在写的同时能加深理解,也希望能帮助小伙伴们理解前端工程化

脑袋嗡嗡的

什么是前端工程化

前端工程化就是使用软件工程的技术和方法对前端开发流程的标准化、工具化、规范化、简单化和自动化。 其主要目的是为了提高开发过程中的开发效率,减少不必要的重复开发时间。

如何做前端工程化

个人觉得要做前端工程化,至少需要具备以下几点:

  • 项目工程化
  • 代码模块化
  • 页面组件化
  • 编码规范化
  • 构建自动化

项目工程化

工程化并非某种技术而是一种思想。
指使用软件工程的技术和方法来进行前端项目的开发、维护和管理。
比如在一个大型的web项目中,整个项目结构复杂而繁琐,需要多个团队配合才能很好地把控,那么我们就需要具备更高层次的思维去组织整个项目的技术选型、各种规范和项目构建等。

代码模块化

代码模块化可以理解为是一组自定义业务的抽象封装,是根据项目的内容来进行封装组合的,比如可以分为登录模块,购物车模块、订单模块。模块可维护性好,组合灵活,方便调用,多人协作互不干扰。

页面组件化

页面组件化指对具体的某个功能进行的封装,比如所有的表格可以封装为table组件来统一使用,以达到组件复用,提高开发效率。

编码规范化

在项目规划初期制定的规范,对于后期的开发极为重要。大体包括的规范有:

  • 命名规范、
  • 编码规范、
  • 结构规范、
  • 接口规范(前后端联调接口规范)、
  • 文档规范、
  • 组件规范、
  • 代码版本管理规范(提交代码备注描述规范)、
  • UI规范(比如:图标库等)
  • ...... 编码规范没有统一的标准,每个团队可以建立自己的一套规范体系。具体做到什么程度,主要还得看你细化到什么程度。

构建自动化

自动化也有很多自动化工具(webpack、rollup、glup......)代替我们来完成,例如:

  • 持续集成
  • 自动化构建
  • 自动化部署
  • 自动化测试

总之就是: 任何简单机械的重复劳动都应该让机器去完成。

前端工程化结语

上述概念其实在软件工程里都是很基础的常识,主要是前端发展快,生态极速变得庞大才从简单的前端页面发展成前端工程化,作为一名前端开发人员我觉得时刻关注技术的发展才是王道,尤其在这个野蛮生长的时刻真的就是不进则退。
作为一个老猿,这几年身边的人真的一年一个样,真的是应了那句话:你要悄悄学习,然后惊艳所有人。真的有的人一段时间不见就学会了一项新技术,有的人几天不撸串喝酒下次再聚薪资翻倍了,有的人同样的起步才一两年不见已经年入100个W。哎~ 这大概就是人生吧!但当你觉察到这些的时候是不是也想着尝试着变好一些呢?

前端工程化就说到这里了吧,接下来补充一点模块化的东西。因为下一篇文章:Vue源码系列(一):Vue源码解读的正确姿势 中需要用到。

模块化方案

下面主要从技术方面解释一下工程化涉及到的点 —> 模块化方案: AMDCMDCommonJSUMDES Modules

AMD

AMD(Asynchronous Module Definition)意思就是"异步模块定义"。在浏览器中,受网络和浏览器渲染的制约,不能采用同步加载,只能采用异步加载。于是 AMD 规范应运而生。
它是一个在浏览器端模块化的开发规范,用于异步加载依赖模块,并且会提前加载。
AMD是RequireJS在推广过程中对模块定义的规范化产出,它是一个概念,RequireJS是对这个概念的实现
AMD采用异步方式加载模块,制定了定义模块的规则,这样模块和模块的依赖可以被异步加载,不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。这和浏览器的异步加载模块的环境刚好适应
📢 注意:浏览器同步加载模块会导致性能、可用性、调试和跨域访问等问题

require主要解决的问题:

  • 文件可以有依赖关系,被依赖的文件需要早于依赖它的文件加载的浏览器
  • js加载的时候浏览会停滞页面的渲染,加载文件越多,页面响应的时间就会越长
  • 异步加载前置

语法:difine(id, dependencies, factory)

AMD只定义了一个函数 "define",它是全局变量 define(id?, dependencies?, factory),参数分别是模块名,依赖,工厂方法

id

是个字符串。它指的是定义中模块的名字,这个参数是可选的。如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字。如果提供了该参数,模块名必须是“顶级”的和绝对的(不允许相对名字)。

dependencies

是个定义中模块所依赖模块的数组。依赖模块必须根据模块的工厂方法优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂方法中

factory

工厂方法。为模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值。

使用 require.js 来实现AMD规范的模块化:

  • require.config() 指定引用路径
  • define() 来定义模块
  • require 来加载模块
define('moduleName',['a', 'b'],function(ma, mb) {
    return someExportValue;
})

require(['a', 'b'], function(ma, mb) {
    //    do something
})

CMD

CMD是另一种模块化方案,它和AMD很类似,不同点在于:AMD推崇依赖前置,提前执行,而CMD推崇依赖就近,延迟执行,CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。

在 CMD 规范中,一个模块就是一个文件。因此经常会用文件名作为模块id,CMD推崇依赖就近,所以一般不在 define 的参数中写依赖,而是在 factory 中写:define(id, deps, factory)
id 和 deps 参数可以省略。省略时,可以通过构建工具自动生成。

注意:带 id 和 deps 参数的 define 用法不属于 CMD 规范,而属于 Modules/Transport 规范。

CMD主要解决的问题:

该规范解决了浏览器环境下如何编写代码实现模块化
还定义了模块化可遵循的一些特征,来支持能共用的模块

  • 模块单一文件
  • 不应引入模块作用域范围内的新的自由变量
  • 懒加载

语法:factory: function(require,exports,module) {}

  • require: factory的第一个参数,用来获取其他模块提供的接口
  • exports: 一个对象,用来向外提供模块接口
  • module: 一个对象,上面存储了与当前模块相关联的一些属性和方法
//    定义没有依赖的模块
define(function(require, exports, module){
    exports.xxx = value
    module.exports = value
})
 
//    定义有依赖的模块
define(function(require, exports, module){
    var module = require('./module.js')
    require.async('./module.js', function(m2) {
        //    do something
    })
    export.xxx = value
})
 
//    引入模块
define(function(require, exports, module){
    const module = require('./module.js')
    module.show()
})

CommonJS

就从CommonJS的出发点说起了,Commonjs的出发点其实就是js没有完善的模块系统,而且标准库较少,还缺少包管理工具。
node.jsCommonjs的最佳实践者。在 node.js 兴起之后,能让 js 在任何地方运行,特别是服务端,也能具备开发大型项目的能力,所以 Commonjs 也就应运而生了。
Commonjs 有四个重要的环境变量为模块化的实现提供支持:moduleexportsrequireglobal
实际使用时,用 module.exports 定义当前模块对外输出的接口(不推荐直接使用exports),用 require 加载模块。
Commonjs 用同步的方式加载模块,在服务端,模块文件够存储在本地磁盘,读取会非常快,所以这样做不会有问题。但是在浏览器端,限制于网络等原因,更合理的加载方式应该是异步加载。

  • 暴露模块:module.exports = fn 或者 exports.xxx = fn (不推荐直接使用exports)
  • 引入模块:const xxx = require('xxxx')

Commonjs规范:

  • 一个文件就是一个模块,拥有单独的作用域;
  • 普通方式定义的变量、函数、对象属于该模块内;
  • 通过require来加载模块;
  • 通过module.exports或者exports来暴露模块中的内容;

注意:

  • 当module.exports和exports同时存在的时候,module.exports会覆盖exports;
  • 当模块内全都是exports是,其实就等同于module.exports;
  • exports其实就是module.exports的子集;
  • 所有代码都是运行在模块作用域,不会污染全局作用域;
  • 模块可以被多次加载,但是只是会在第一次运行的时候加载。然后运行结果就会被缓存了。以后的加载就直接读取缓存的结果;
  • 模块加载的顺序是按照代码出现的顺序同步加载的;
  • __dirname 代表当前模块所在的文件路径
  • __filename 代表当前模块文件所在的文件路径+文件名

UMD

UMD 就是一种思想,它是一个整合了AMD、CMD和Commonjs规范的方法。define.amd / define.cmd / module 等判断当前支持什么方式,都不行就挂载到 window 全局对象上面去

运行的原理

UMD会先判断是否支持Node.js的模块(export是不是存在)。存在则使用node.js的模块方式。
再判断是不是支持AMD(define是不是存在)。存在则使用AMD方式加载模块。

(function (root, factory) { 
    if (typeof define === 'function' && (define.amd || define.cmd)) {
        //AMD,CMD 
        define(['b'], function(b){ 
            return (root.returnExportsGlobal = factory(b)) 
        });
    } else if (typeof module === 'object' && module.exports) {
        //Node, CommonJS之类的 
        module.exports = factory(require('b'));
    } else {
    //公开暴露给全局对象 
    root.returnExports = factory(root.b); 
    } 
}(this, function (b) {
    return {}; 
}));

ES Module(ESM)

ESM 在语言标准的层面上,实现了模块化功能,而且实现的相当简单,旨在成为浏览器和服务器通用的模块化解决方案,其模块化功能主要由俩个命令构成:exportsimport

  • export命令由于规定模块的对外接口
  • import命令用于输入其他模块的功能

其次ESM还提供了export default的命令,为模块指定默认输出,对应的 import 语句不需要大括号,这也更接近AMD的引用写法。

ESM模块不是对象,import命令被JavaScript引擎静态分析,在编译的时候就引入模块代码。而不是在代码运行时加载,所以无法实现条件加载。也就使得静态分析成为可能。

export

export 可以导出的是对象中包含多个属性、方法,而 export default 只能导出一个可以不具名的函数,我们可以用 *import *引入。
同时我们也可以直接使用 require 使用,原因是 webpack启用了 server 相关。

import

import { fn } from './xxx'    //  export 导出方式
import fn from './xxx'           //  export default 导出方式

总结:

AMD 是异步加载依赖模块,并会提前加载且并可行加载多个模块。主要是在浏览器环境下应用,require.js 是遵守 AMD 规范的模块化工具。它通过 define() 定义声明,通过 require('',function() {}) 来加载。
缺点是:开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅;不符合通用的模块化思维方式,是一种妥协的实现。

CMD和AMD很相似,尽量保持简单,并与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。
优点:依赖就近,延迟执行 可以很容易在 Node.js 中运行;
缺点:依赖 SPM 打包,模块的加载逻辑偏重;

AMD 和 CMD 的区别

  1. AMD 是 RequireJS 在推广过程中对模块定义的规范化产出,CMD 是 SeaJS 在推广过程中对模块定义的规范化产出;
  2. 对于依赖的模块,AMD 是 提前执行,CMD 是 延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同);
  3. CMD 推崇 依赖就近,AMD 推崇 依赖前置
  4. AMD 的 API 默认是 一个当多个用,CMD 的 API 严格区分,推崇 职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都 简单纯粹「注:这个AMD 和 CMD 的区别来自于 玉伯大神 的回答」

CommonJS是同步加载,主要是 node.js(服务端应用)的模块化机制。通过 module.exports 导出声明,然后通过 require('') 加载。
每个文件都是一个模块,有自己的独立的作用域,且文件内的变量、属性、函数等不能被外界访问。
node 会将模块缓存,在第二次加载的时候会从缓存中获取。

UMD 是一个整合了AMD、CMD和Commonjs规范的方法。通过判断来决定当前支持什么方式。

ES Module 模块化加载时,通过 exports default 导出。用 import 来导入,可以通过对导出内容进行解构。ES Module 模块运行机制与 Commonjs 运行机制不一样。js引擎对脚本静态分析的时候,遇到模块加载指令后会生成一个只读引用。等到脚本真正执行的时候。才会通过引用模块中获取值,在引用到执行的过程中,模块中的值发生变化,导入的这里也会跟着发生变化。ES Module 模块是动态引入的。并不会缓存值。模块里总是绑定其所在的模块。



到此本篇文章结束,如果对你有用还希望点赞加关注🙏🙏 ,非常感谢。