前端模块化
1.什么是前端模块化
- 将复杂的程序根据规则或者规范拆分成若干模块,一个模块包括输入和输出
- 模块化的内部数据和实现是私有的,对外暴露一些接口与其他模块进行通信
2.前端模块化的背景
- 前端模块化是一种**
标准,不是实现**
- 理解模块化是理解前端工程化的**
前提**
- 前端模块化是前端项目规模化的必然结果
3.脚本和模块的区别
有很多同学会对脚本和模块之间产生一定的混淆,我这里通过一张图来帮助大家区分两者的不同。
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加载模块,通过exports或module.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原理分析
因此我们可以总结出如下常用的命令:
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,正式入门前端工程化,成为前端工程化开发领域的实践者!