模块化是把一个复杂的程序,按照一定的规则或者规范拆分成若干模块
简单做个类比的话,模块就好比我们玩的乐高积木里面那一个个小零件
对于一个模块来说,它有内部的完整实现 ,对外则暴露一些接口可以和其他模块做交互
模块化历史
闭包
写过JS肯定都了解这个概念,它允许我们访问非当前函数作用域的变量,从而给不同的变量一个独有的命名空间 ,同时也允许了多个命名空间之间相互访问,这也是最早的一种模块化形式。
js
function lastName() {
const lastName = "ge"
return () => {
return lastName;
}
}
function firstName() {
const firstName = "poem"
return () => {
return firstName;
}
}
// 借助闭包,简单粗暴地处理局部变量和依赖关系的问题
const name = lastName()() + '' + firstName()()
CMD&AMD&UMD
随着模块化工具的发展,过程中诞生了CMD
、AMD
和UMD
三种模块化方案
现在还在库打包 的时候会用UMD
,其他两种很少用了
AMD
- 依赖于
requirejs
,专门跑在浏览器环境 中,主要关注异步加载 - 使用
define
定义模块,require
异步加载模块
js
// 定义模块
define(['module1', 'module2'], function(module1, module2) {
// 模块代码
function init() {
module1.foo();
module2.bar();
}
// 导出模块
return {
init: init
};
});
// 使用模块
require(['myModule'], function(myModule) {
myModule.init();
});
CMD
- 依赖
seajs
,专门跑在浏览器环境 中,更注重就近依赖 - 使用
define
定义模块,require
就近声明模块依赖关系
js
// 模块定义
define(function(require, exports, module) {
// 引入依赖模块
var module1 = require('./module1');
var module2 = require('./module2');
// 模块代码
function init() {
module1.foo();
module2.bar();
}
// 导出模块
exports.init = init;
});
// 使用模块
var myModule = require('./myModule');
myModule.init();
UMD
- 通用 模块化解决方案,兼容支持
CMD
和AMD
规范环境 - 支持全局变量方式引入模块
- 需要构建/打包工具生成代码
js
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['dependency1', 'dependency2'], factory);
} else if (typeof exports === 'object' && typeof module === 'object') {
// CommonJS
module.exports = factory(require('dependency1'), require('dependency2'));
} else {
// 全局变量 (浏览器环境)
root.MyModule = factory(root.Dependency1, root.Dependency2);
}
}(typeof self !== 'undefined' ? self : this, function(dependency1, dependency2) {
// 模块代码
var myModule = {};
function foo() {
// 使用依赖模块
dependency1.bar();
dependency2.baz();
console.log('foo');
}
myModule.foo = foo;
return myModule;
}));
CommonJS和ES6 Modules
现在通用的模块化方案包括CommonJS
和ES6 Modules
,主要的区别是
CommonJS
是node中的模块化方案,用在node端ES6 Modules
是浏览器端 的模块化方案(简称ESM
)
CommonJS
CommonJS
用module.exports
对外暴露当前的文件的内容,本质上,每个文件都有一个module
变量,里面就存放了当前文件的路径 、exports属性等
js
Module {
'8': [Function: internalRequire],
id: '.',
path: '/Users/shige/Desktop/NodeProjects/testFiles',
exports: {},
parent: null,
filename: '/Users/shige/Desktop/NodeProjects/testFiles/test6.js',
loaded: false,
children: [],
paths: [
'/Users/shige/Desktop/NodeProjects/testFiles/node_modules',
'/Users/shige/Desktop/NodeProjects/node_modules',
'/Users/shige/Desktop/node_modules',
'/Users/shige/node_modules',
'/Users/node_modules',
'/node_modules'
]
}
想要导入别的文件exports的内容,就需要用require
引入,并且可以解构赋值
js
const { hello } = require("../test.js")
ES6 Modules
ESM是专门的浏览器端 的模块化方案,和CommonJS的区别是,它的导入和导出都分了默认 和按需两种
按需导出/导入
用export
和import
关键字实现,对于按需导出的,导入的时候要用大括号,类似解构赋值的形式去做
js
export function add(a, b) {
return a + b
}
import { add } from "./calculate"
默认导出/导入
默认导出用export default
,后面用大括号 把所有要导出的内容给包裹起来,相当于导出了一个对象
导入的时候,可以给这个默认导入重命名一下,使用其中的属性或者方法,用.去访问即可
js
export default {
name: "poem",
add: function(a, b) {
return a + b
}
}
import test from "./calculate";
console.log(test.add);
模块化工具介绍
模块化发展到现在,出现了一些主流工具,包括:
- webpack
- vite
- rollup
webpack
目前使用率最广 的前端打包工具,Vue的脚手架vue-cli
和React脚手架CRA
都是基于webpack
的
webpack的特点包括以下:
- 打包从一个
入口entry
开始,以递归的形式找到所有依赖模块,构建出一个依赖关系图 - 默认只能处理
js文件
,想处理其他类型文件,需要使用loader
- 不同类型文件走不同流水线 ,最后输出结果,可以修改输出
output
或者接入插件plugin
指定额外功能(打包优化、资源管理等)
webpack的缺点就是,当项目越大的时候,处理时间会越长,因为是要抓取构建整个应用,才能提供服务
vite
vite的核心和webpack有以下区别:
- devServer开发服务器,基于
原生ESM
实现 - 打包能力基于
rollup
vite相比于webpack速度更快,因为在打包的时候把代码分成了依赖 和源码两种类型,只有当浏览器请求需要源码的时候再去转换并提供源码
rollup
一个专门的JS模块打包器 ,支持ES6模块原生treeshaking
等一些优化功能,更偏向于库打包
模块化工具使用
webpack
课程里面提到的只有两个重点:configureWebpack
和chainWebpack
,以及plugin
和loader
区别
configureWebpack和chainWebpack
简单地说,configureWebpack
是简单的配置方式,不同于webpack官方,具体要看Vue文档提供的配置项
chainWebpack
基于webpack-chain
,是一个链式操作 的配置属性(有点像jquery
),最后返回args
配置属性
最基本的修改方式包括
tap
:修改某个属性值add
:在属性值末尾添加新的值
例如我们可以通过配置项修改运行时项目的网页标签名称
js
// 对于我们想修改的webpack配置项,可以在webpack-config.js里面找到要修改的配置项
// 具体链式操作时候的配置项,可以看看webpack-config.js的注释
// 这里以修改HTML的title为例
chainWebpack: (config) => {
config.plugin('html').tap((args) => {
args[0].title = '测试title'
return args
})
}
修改了之后可以用vue-cli
里面的inspect
命令输出webpack配置项,可以看到对应的配置项被修改了
js
"scripts": {
"inspect": "vue-cli-service inspect > webpack-config.js"
}
plugin和loader
和plugin以及loader有关的有三个术语
模块
module
:一般一个module
为一个文件,通常指的是js文件块
chunk
:一个块是由打包工具通过配置生成的,包含了一组相关的模块打包文件
bundle
:构建工具在打包过程中最终输出的文件,可在浏览器中运行
loader
loader是加载器 ,因为webpack默认只处理js文件,所以其他类型的文件需要loader处理
例如我们可以做这样的操作:对项目中的txt文件 的文本做替换,使用如下的loader
js
const loadUtils = require("loader-utils");
module.exports = function (source) {
const options = loadUtils.getOptions(this);
const content = options.content;
source = source.replace(/hello world/, content);
return `module.exports = '${source}'`;
};
在vue.config.js
中添加如下的loader
js
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
transpileDependencies: true,
chainWebpack: (config) => {
config.module
.rule("txt-loader") // 规则名称
.test(/\.txt$/) // 匹配文件
.use("txt-loader")
.loader("./src/loaders/textLoader") // loader地址
.options({ content: "你好 哈哈" }) // 配置项,即后续更新的文本
.end();
},
});
最后就可以使用了
js
const text = require("./test.txt");
console.log(text); // hello world会被替换成你好 哈哈
plugin
plugin是插件 ,可以为打包工具提供一些附加的功能
例如我们可以在编译完成的时候在控制台上添加一些输出,可以创建如下的plugin
,里面需要带有一个apply
方法,该方法会被自动执行
js
class LogPlugin {
constructor(options) {
this.content = options.content;
}
// compiler是webpack编译器的实例
apply(compiler) {
// compiler里有若干生命周期,这里用了done表示编译结束
compiler.hooks.done.tap("logPlugin", (compilation) => {
console.log(this.content);
});
}
}
module.exports = LogPlugin;
接着就是在vue.config.js
中添加如下的plugin
js
const { defineConfig } = require("@vue/cli-service");
const LogPlugin = require("./src/plugins/logPlugin");
module.exports = defineConfig({
transpileDependencies: true,
chainWebpack: (config) => {
config
.plugin("LogPlugin") // 插件名字
// 第一个参数是插件,第二个是传入的参数,因为使用的时候都是args[0].XXX
.use(LogPlugin, [
{
content: "hello shi",
},
])
.end();
},
});
项目运行时候就能看到编译成功后的输出了
vite
vite作为新的打包工具,其速度快主要靠这几个方面
- 基于原生ESM
- 按需编译:只有当使用到某个模块,才会进行编译
- 模块预构建:对于非ESM的模块,会提前预构建 ,可以在
node_modules/.vite/deps
文件夹中看到
esbuild
vite使用的就是esbuild
实现模块预构建,这个预构建其实就是把CommonJS这些模块编译成ESM,速度很快