一、前言
本人对于前端工程化知识了解相对来说不够深入,故将本文作为掘金第一篇文章。一是为了记录自己的学习进度与知识、二是希望自己通过这种方式来督促自己。如遇有问题的地方望各位大佬能加以指正。
二、为什么要使用工程化去管理代码?
在实际的企业前端项目中会面临着以下真实的的问题:
- 项目的量级急速增加
- 项目数量扩大
- 项目复杂程度高
- 项目团队人数增长
那么针对于这么多的痛点有着以下的解决方案
- 代码量级 可以利用模块化的思想复用模块来解决(CommonJs与ESModule)、npm
- 项目数量扩大一般通过研发平台、研发脚手架来进行项目管理。
- 项目复杂程度可通过工程手脚架来统一解决不同技术栈之前的工程差异
- 团队人数增长通过研发平台、研发脚手架来团队协同。
企业前端工程化应用场景
- 项目研发模式升级(模块化 +MVVM)
- 工程脚手架
- 项目性能优化
综上所述,前端工程化是对于整个前端开发的体系有着必不可少的作用的,下面进入正题模块化。
三、前端工程化之模块化
什么是前端模块化?
前端模块化是指将复杂程序根据规范拆分为若干模块。
前端模块化的发展历程?
- 最开始而言,是将简单函数进行功能化定义达到功能复用的效果。这也是模块化的雏形。
js
function getData(){
return {
data:{
name:"xxx",
age:18,
gender:"男"
}
}
}
function handle(data,key){
return data.data[key]
}
它根据函数进行封装然后通过script标签将js脚本引入html中。这样子虽然简单,但是会有点问题(函数会直接挂载到浏览器window对象 身上,这样就会造成全局对象的环境污染)。
- 针对于全局环境污染问题,于是就有了第二种方案。
js
let module = {
x:1,
setX(value){
x = value;
},
getX(){
return x;
}
}
window.__Module = module;
利用对象来将属性及各个函数封装并挂载至window对象的一个属性下,这样子可避免变量污染全局环境。但仔细看这种方法还是有点弊端(我们仍能通过**__Module**来进行篡改该内部对象属性)。
- 对于以上问题我们可以利用闭包IIFE来轻松解决
js
(function(window){
x=1
function setX(value){
x = value;
}
function getX(){
return x;
}
window.__Module={
value:x,
setX,
getX
}
})(window)
window.__Module.setX(2)
console.log("直接访问Value=============>"+window.__Module.value);
console.log("getX访问Value=============>"+window.__Module.getX());
结果如下所示:

这样就解决的全局变量环境污染,以及作用域内不被轻易篡改。当然如果出现模块间相互依赖(例如A需要引用B模块的函数)我们可以这样来做。
js
//模块B
(function(window){
function sum(a,b){
return a+b
}
window.__ModuleB={
sum
}
})(window)
//模块A
(function(window,moduleB){
x=1;
let c = moduleB.sum(1,2);
})(window,window.__ModuleB)
注意:B脚本文件在HTML中利用script引用必须在A之前。
这种闭包的做法虽然能解决模块化问题,但是它本身针对大型项目还是会有以下问题:
- 模块依赖较多时,代码可读性差
- 无法支持大规模模块化开发
- 语法简陋,维护成本高。
至此,对于模块化而言出现了许多的模块化规范。下面我主要围绕CommonJs规范和EsModule规范来进行分析。
CommonJs规范
- Node.js默认模块化规范,每个文件就是一个模块,有自己的作用域
- Node中CJS模块加载采用同步加载方式
- 通过require进行加载,通过export与module.exports或exports进行导出
- 模块可以多次加载,第一次加载时会运行模版,模版输出结果会被缓存,再次记载时,会从缓存结果中直接读取模块输出结果。
通过一个简单例子我们来看一下规范:
js
//b模块
function sum(a,b){
return a+b;
}
//通过module.exports导出函数。
module.exports = {
sum
}
//a模块
const b = require('./b.js')
const result = b.sum(1,2);
console.log(result);
这里我们也可以利用exports.sum = sum;进行导出
js
//b模块
function sum(a,b){
return a+b;
}
//通过exports导出函数
exports.sum = sum
module.exports与exports的区别是什么?
在 Nodejs 中,文件和模块是一一对应的(每个文件被视为一个独立的模块 ),在根据入口node文件开始运行时,他会从入口文件开始构建他的模块依赖关系,并且在每个模块上面去挂载一些函数或者属性(require方法、module对象),这也是我们可以在node环境下每个js文件里使用他们的原因。在CommonJs导出规范中,他实际导出的是module.exports里面的内容。exports是对于module.exports对象的引用(exports=module.exports = {})。我们可以根据以下例子来看。
js
//模块B
function sum(a,b){
return a+b;
}
module.exports={
sum
}
//通过exports导出函数(改变了指向)
exports={
name:"123"
}
//模块A
const b = require('./b.js')
console.log(b);

可以看到他最终是直接导出module.exports中的内容。所以我们一般使用export. 的方式对原有导出模块对象添加属性或者函数来进行增量式导出。
我们的CommonJs如果在浏览器可以运行吗? 答案是可以的,但是浏览器不能识别require与module.exports等语法规范,所以我们需要browserify库来进行转换。
js
//模块B
function sum(a,b){
return a+b;
}
module.exports={
sum
}
//模块A
const b = require('./b.js')
console.log(b);
转换后:
js
(function () {
function r(e, n, t) {
function o(i, f) {
if (!n[i]) {
if (!e[i]) {
var c = "function" == typeof require && require; if (!f && c) return c(i, !0); if (u) return u(i, !0);
var a = new Error("Cannot find module '" + i + "'"); throw a.code = "MODULE_NOT_FOUND", a
}
var p = n[i] = { exports: {} };
//require方法传递
e[i][0].call(p.exports, function (r) { var n = e[i][1][r]; return o(n || r) }, p, p.exports, r, e, n, t)
} return n[i].exports
} for (var u = "function" == typeof require && require, i = 0; i < t.length; i++)o(t[i]); return o
}
return r
})()(
//调用参数
{
1: [function (require, module, exports) {
const b = require('./b.js')
console.log(b);
}, { "./b.js": 2 }], 2: [function (require, module, exports) {
function sum(a, b) {
return a + b;
}
module.exports = {
sum
}
}, {}]
}, {}, [1]);
转换过程其实也就是构建模块间依赖关系,利用重写后的require函数获取依赖模块导出数据。最后再进行模块执行。(整个过程也就是挂载require函数、module、exports)
ESModule规范
- ESModule设计理念希望在编译时确认依赖关系以及输入输出。
- CommonJs必须在运行时才能确认依赖和输入输出。
- ESM通过import ... from '*' 加载模块,通过export 和 export default输出模块。
下面请看ESModule的一个例子:
js
//模块B:
export default function sum(a,b){
return a+b;
}
//模块A
import sum from './b.js'
console.log(sum(1,2));
对于浏览器而言,高版本是支持ESModule模块化的。我们可以这样引用它。
js
<script type="module" src="./a.js"></script>
ESModule也可以进行导出多个值:
js
//模块B
export default function sum(a,b){
return a+b;
}
export function sub(a,b){
return a-b;
}
//模块A:export default可直接接,export需要利用对象解构来接
import sum,{sub} from './b.js'
console.log(sum(1,2));
console.log(sub(5,3));
CommonJs与ESModule规范对比
- CommonJs是运行时加载并确认依赖关系,ESM时编译时即可确认依赖关系。
js
//即运行时,执行表达式加载相对应模块依赖
const {sum} = require('./a'+'b.js')
-
CommonJs是单个值导出,ESM输出多个值
CommonJs最后导出的只是Module.exports内容,但是ESM可以通过export与export default导出多个
-
CommonJs模块为同步加载,ES6 Module支持异步加载。
js
//import(文件名)可异步来加载模块
import('./b.js').then(res=>{
console.log(res);
})
- CommonJs的this是当前模块,ES6 Module的this是undefined
🚀浏览器模块化局限性
- 缺乏模块管理,模块整体杂乱
- 性能加载慢,无法使用于大型项目
🛰️局限性解决方案npm、webpack
npm:主要是一个模块(包)管理工具,它负责帮助我们管理整个模块。 webpack: 主要是利用它提供loader和plugins完成功能扩展,以提升开发效率以及性能优化。
第一次发文,如果有问题劳烦各位大佬指正诺。谢谢