3

前端工程化三部曲之基础篇--模块化技术

 1 year ago
source link: https://jelly.jd.com/article/639be9a0abf18f005786c57f
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

JELLY | 前端工程化三部曲之基础篇--模块化技术

前端工程化三部曲之基础篇--模块化技术
上传日期:2022.12.16
本节从前端模块化发展历史,衍生出了CommonJS规范、AMD规范、CMD规范到现在的ESModule,成为浏览器和服务器通用的模块解决方案。再加上npm管理工具和webpack打包编译工具的诞生,一举突破了前端工程化的关键技术。

前端模块化

1.什么是前端模块化

  • 将复杂的程序根据规则或者规范拆分成若干模块,一个模块包括输入和输出

  • 模块化的内部数据和实现是私有的,对外暴露一些接口与其他模块进行通信

2.前端模块化的背景

  • 前端模块化是一种标准,不是实现
  • 理解模块化是理解前端工程化的前提
  • 前端模块化是前端项目规模化的必然结果

3.脚本和模块的区别

有很多同学会对脚本和模块之间产生一定的混淆,我这里通过一张图来帮助大家区分两者的不同。

11d36b920f7b79f2.png

4.前端模块化的进化过程

4.1 全局function模式:将不同功能封装成不同的全局函数

  • 缺陷:容易引发全局命名空间冲突,而且模块成员之间看不出直接关系
// 所有function都是挂在window下面的
funtion api(){
  return {
    xxx
  }
}

function handle(data, key){
  return xxx
}

function sum(a, b){
  return a + b;
}

const data = api();
const a = handle(data, 'a')

4.2 全局namespace模式

  • 作用:减少了全局变量,解决了命名冲突

  • 缺陷:存在数据安全的问题,外部可以直接修改模块内部数据

window.__Module = {
  x: 1,
    api(){
        xxx
    },
    handle(){
      xxx
    },
    sum(a,b){
        return a + b
    }
}

const module = window.__Module
consr data = module.api()

console.log(module.x) // 1
module.x = 2

4.3 IIFE模式:匿名函数自调用 -- 闭包

  • 作用:通过自执行函数创建闭包,解决私有化的问题,外部只能通过暴露的方法操作
  • 缺陷:无法解决模块间相互依赖的问题
(function(window){
  var x = 1;

  function api(){
    xxx
    }

  function setX(v){
    x = v
    }

  function getX(){
    return x
  }

  window.__Module = {
    x,
    setX,
    getX,
    api,
  }
})(window)

const m = window.__Module

// 这里改的是函数作用域内变量的值
m.setX(10)
console.log(m.getX()) // 10

// 这里改的是对象属性的值,不是修改的模块内部的data
m.x = 2
console.log(m.getX()) // 10

4.4 IIFE模式增强,支持传入自定义依赖

  • 作用:通过模块间参数的传递,来实现解决模块间的依赖问题

    • 多依赖传入时,代码阅读困难
    • 无法支持大规模的模块化的开发
    • 无特定语法支持,代码简陋

A: __Module_API模块

(function(global){
  var a = 1;
  function api(){
    return {
      code: 0,
      data: {
        a,
        b: 2
      }
    }
  }
  function handle(data, key){
    return data.data[key]
  }
  global.__Module_API = {
    api,
    handle
  }
})(window)

B:__Module模块

(function(global, moduleAPI){
  function sum(a, b){
    return a + b;
  }
  global.__Module = {
    api: moduleAPI.api,
    handle: moduleAPI.handle,
    sum,
  }
})(window, window.__Module_API)

const module = window.__Module
const data = module.api.api()
const a = module.api.handle(data, 'a')

通过将Module_API模块作为入参传入到Module模块中,实现在Module模块中引用依赖Module_API的一些模块方法

5.前端模块化的好处

  • 减少了全局变量,解决了命名冲突
  • 能够更好的分离,实现按需加载
  • 有更高复用性和更高可维护性

CommonJS模块化规范

1.CommonJs规范介绍

  • Node.js默认的模块化规范,每个文件就是一个模块,有自己的作用域

  • Node中CJS模块加载采用在服务器端运行时同步加载方式,在浏览器端提前编译打包处理方式

  • 通过require加载模块,通过exportsmodule.exports输出模块

2.CommonJS规范特点

  • 所有代码都运行在模块作用域,不会污染全局作用域
  • 模块可以多次加载,第一次加载时会运行模块模块输出结果会被缓存,再次加载时,会从缓存结果中直接读取模块输出结果
  • 模块加载的顺序,按照其在代码中出现的顺序

  • CommonJS 规范的核心变量: exports、module.exports、require

    • CommonJS规范规定,每个模块内部,module变量代表当前模块。
    • 这个module变量是一个对象,它的exports属性(module.exports)是对外的接口, 负责对模块中的内容进行导出
    // lib.js
    var counter = 3;
    function incCounter() {
      counter++;
    }
    module.exports = {
      counter: counter,
      incCounter: incCounter,
    };
    • require 函数基本功能就是读入并执行一个JavaScript文件,然后返回该模块的exports对象。
    const mod = require('./lib')
  • 模块输出的值是值的拷贝,类似IIFE方案中的内部变量

3.CommonJS加载机制

CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值

请看下面这个模块文件lib.js的例子。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

上面代码输出内部变量counter和改写这个变量的内部方法incCounter。然后,在main.js里面加载这个模块。

// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。比如改成下面的这种写法

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

这样输出的counter属性就变成了一个取值器的函数,就可以正确读取到内部变量counter的变动。

ESModule模块化规范

1.简单了解AMD规范和CMD规范

1.1 AMD规范:

  • AMD规范采用非同步加载模块,允许指定回调函数(针对commonjs同步而诞生的规范)
  • Node模块主要用于服务器编程,模块文件通常都位于本地硬盘,加载起来速度比较快,所以适用于CommonJS的这种同步加载

  • 但是浏览器环境下,模块需要请求获取,要从服务端加载模块,所以适用于异步加载,一般采用AMD规范

  • require.js是AMD的一个具体实现库

AMD基本语法 -- 定义暴露模块
//定义没有依赖的模块
define(function(){
   return 模块
})

//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
   return 模块
})
AMD基本语法 -- 引入使用模块
require(['module1', 'module2'], function(m1, m2){
   使用m1/m2
})

1.2 CMD规范

  • CMD专门用于浏览器端,整合了CommonJS和AMD的优点,模块的加载是异步的,模块使用时才会加载执行
  • Sea.js是CMD规范的一个实现
CMD基本语法 -- 定义暴露模块
//定义没有依赖的模块
define(function(require, exports, module){
  exports.xxx = value
  module.exports = value
})

//定义有依赖的模块
define(function(require, exports, module){
  //引入依赖模块(同步)
  var module2 = require('./module2')
  //引入依赖模块(异步)
  require.async('./module3', function (m3) {
    console.log(m3)
  })
  //暴露模块
  exports.xxx = value
})
CMD基本语法 -- 引入使用模块
define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

注意:AMD 和 CMD 规范现在已经不怎么去用了。AMD和CMD最大的问题是没有通过语法升级解决模块化(它们定义模块还是通过调用js的方式定义一个模块,它没有办法对模块进行规模化的引用)

所以我们现在主流的使用:node环境下用commonjs,浏览器环境下用ESModule

2.ESModule规范介绍

  • ESModule设计理念是希望在编译的时就确定模块的依赖关系及输入输出
  • CommonJS和AMD都只能在运行时才能确定依赖和输入、输出

举例说明:

// ES6模块
import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

3.ESModule模块化语法(结合日常实战开发)

3.1 export命令

工作编码的时候我们常用的一些公共导出方法(例如utils.js文件),经常会采用下面两种方式来书写

第一种写法:
// 获取url上指定参数的值
export const getQueryString = (name) => {
  console.log('getQueryString...')
};
// 随机数生成
export const getRandomString = (len) => {
  console.log('getRandomString...')
};
// 获取cookie
export const getCookie = (objName) => {
  console.log("getCookie....")
};
第二种写法:(推荐)

优先推荐使用这种写法。因为这样我们就可以在脚本尾部,一眼看清楚输出了哪些变量。

而且方便通过as的关键字可以对输出的变量进行重命名。

// 获取url上指定参数的值
const getQueryString = (name) => {
  console.log('getQueryString...')
};
// 随机数生成
const getRandomString = (len) => {
  console.log('getRandomString...')
};
// 获取cookie
const getCookie = (objName) => {
  console.log("getCookie....")
};

export { getQueryString as getQuery, getRandomString, getCookie }

3.2 export default命令

从前面的实战我们可以看出,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。

为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

1)实战用法举例1:

导出ace接口文件配置:

// api/ace文件
import * as ace from '@/api/common/color.jd.com';

// ace.jd.com
const getACEData = async (id: string) => {
  return await ace.get({
    url: `//api.m.jd.com/client.action?xxx`,
  });
};
const api = {
  getACEData,
};
export default api;

引用ace文件:(import命令可以为该函数指定任意名字。)

import ACE_API from '@/api/ace';
const res = await ACE_API.getACEData(123);

注意:需要注意的是,这时import命令后面,不使用大括号。

2)实战用法举例2:

如果想在一条import语句中,同时输入默认方法和其他接口,可以写成下面这样。

import _, { each, forEach } from 'lodash';

对应上面代码的export语句如下

export default function (obj) {
  // ···
}

export function each(obj, iterator, context) {
  // ···
}

export { each as forEach };

比如我们平时用react框架开发时,也会用到这种写法:

import React, { useEffect, useState } from 'react';

3.3 import命令

使用import命令有一些重要的关键点,这里给大家列举阐述一下:

  • 可以使用as关键字将输入变量重命名
import { lastName as surname } from './profile.js';
  • import命令是只读的,不允许在加载模块的脚本里面,改写接口。
import {a} from './xxx.js'

a = {}; // Syntax Error : 'a' is read-only;
  • import命令具有提升效果,会提升到整个模块的头部,首先执行

下面的代码不会报错,因为import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。

foo();

import { foo } from 'my_module';
  • 多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。

  • 由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

// 报错
import { 'f' + 'oo' } from 'my_module';

// 报错
let module = 'my_module';
import { foo } from module;

// 报错
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}

3.4 模块的整体加载

我们可以用星号(*)指定一个对象,所有输出值都加载在这个对象上面。

日常开发举例:

比如我们开发所用到的公共方法类文件(utils.js文件)

// utils.js
// 获取url上指定参数的值
const getQueryString = (name) => {
  console.log('getQueryString...')
};
// 随机数生成
const getRandomString = (len) => {
  console.log('getRandomString...')
};
// 获取cookie
const getCookie = (objName) => {
  console.log("getCookie....")
};

export { getQueryString as getQuery, getRandomString, getCookie }

那么我们整体加载的写法可以更改如下:

import * as utils from '@/utils/utils';
utils.getQueryString('shopId')

4.CommonJS和ESModule规范对比

这里主要对上面两种不同的规范进行一个对比总结:

4.1 CommonJS模块输出的是值的拷贝,ES6模块输出值的引用

ESModule举例:输出的a是一个地址,这个值变化后面是跟着变化的

// test.js
export let a = 1;
export function plus(){
  a++;
}

// entry.js
import { a , plus } from './test.js'

console.log(a); // 1
plus();
console.log(a); // 2

CommonJS是对值是进行拷贝的,例如这里是对值a的一个拷贝

// test.js
let a = 1;

exports.a = a;
exports.plus = function(){
  a++;
}
exports.get = function(){
  return a;
}

// entry.js
const { a, plus, get } = require('./test.js')
console.log(a) // 1
plus();
console.log(a) // 1
console.log(get()) // 2

4.2 CommonJS模块运行时加载,ES6模块是编译时输出接口

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

4.3 CommonJS模块为同步加载,ES6模块支持异步加载

// ESModule 可以通过promise的方式异步加载
import('./test.js').then(mod =>{
  console.log('mod', mod)
})

4.4 CommonJS中this是当前模块,ES6模块的this是undefined

// commonjs
console.log(this === module.exports)

5. 浏览器模块化的局限

缺乏模块管理能力,模块分散在各个项目中 ---- npm统一管理

性能加载慢,无法在大型项目中直接使用 --- webpack性能优化

npm + webpack原理

前端工程化之关键技术npm + webpack原理

1.npm包管理工具

1.1 npm诞生背景

  • npm由程序员Isaac发明
  • 初步思路
    • 集中管理所有模块,所有模块都上传到仓库(registry)
    • 模块内创建package.json标注模块的基本信息
    • 通过npm publish发布模块,上传到仓库(registry)
    • 通过npm install安装模块,模块安装到node_modules目录

1.2 npm介绍

  • npm解决的核心问题是模块管理问题
  • npm规范:package.json管理模块信息,node_modules保存依赖

1.3 npm原理分析

11d36b920f7b79f2.png

因此我们可以总结出如下常用的命令:

npm init创建模块,npm install 安装模块,npm publish发布模块

npm link本地开发,npm config 调整配置,npm run调用scripts

1.4 npm局限

  • npm只能解决模块的高效管理和获取问题

  • npm无法解决性能加载问题

  • 模块化发明后,制约其广泛应用的因素就是性能问题

2.webpack代码编译工具

2.1 webpack诞生背景

  • Webpack 2012年3月10号诞生,作者是Tobias
  • webpack的出现模糊了任务和构建的边界,使之融为一体

webpack诞生之前专门有一些工具是做任务的,例如gulp或者grunt。任务就是每一步要干什么东西,由这个任务的引擎来决定。构建是由其他工具来决定

2.2 webpack原理

  • 最初的webpack核心解决的是代码合并与拆分
  • webpack的核心理念是将资源都视为模块,统一进行打包和处理
  • webpack提供了loader和plugins完成功能扩展

本节从前端模块化发展历史,衍生出了CommonJS规范、AMD规范、CMD规范到现在的ESModule,成为浏览器和服务器通用的模块解决方案。再加上npm管理工具和webpack打包编译工具的诞生,一举突破了前端工程化的关键技术。

作为前端工程化系列的第一篇文章,希望让大家认识到前端工程化并没有那么复杂,就是我们平时开发工作中经常用到的知识点,学习起来也会相对容易一些。接下来的进阶篇我会带领大家玩转webpack,正式入门前端工程化,成为前端工程化开发领域的实践者!

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules

https://es6.ruanyifeng.com/#docs/module

https://es6.ruanyifeng.com/#docs/module-loader

https://github.com/amdjs/amdjs-api/wiki/AMD-(%E4%B8%AD%E6%96%87%E7%89%88)

https://github.com/seajs/seajs/issues/242


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK