1、 npm init -y 创建package.json文件
2、创建创建minipack.js
3、创建测试文件夹和文件
4、下载依赖npm i babylon babel-traverse babel-core babel-preset-env -D
5、启动项目:node minipack.js
babel-preset-env:这种现象是由于在 .babelrc 文件中设置了env 选项,需要插件 babel-preset-env 处理
mini版webpack未涉及loader和plugin等复杂功能,是一个非常简单的例子。
mini版的webpack打包流程
const fs = require('fs');
const path = require('path');
// babylon解析js语法,生产AST 语法树
// ast将js代码转化为一种JSON数据结构
const babylon = require('babylon');
// babel-traverse是一个对ast进行遍历的工具, 对ast进行替换
const traverse = require('babel-traverse').default;
// 将es6 es7 等高级的语法转化为es5的语法
const { transformFromAst } = require('babel-core');
// 每一个js文件,对应一个id
let ID = 0;
// filename参数为文件路径, 读取内容并提取它的依赖关系
function createAsset(filename) {
const content = fs.readFileSync(filename, 'utf-8');
// 获取该文件对应的ast 抽象语法树
const ast = babylon.parse(content, {
sourceType: 'module'
});
// dependencies保存所依赖的模块的相对路径
const dependencies = [];
// 通过查找import节点,找到该文件的依赖关系
// 因为项目中我们都是通过 import 引入其他文件的,找到了import节点,就找到这个文件引用了哪些文件
traverse(ast, {
ImportDeclaration: ({ node }) => {
// 查找import节点
dependencies.push(node.source.value);
}
});
// 通过递增计数器,为此模块分配唯一标识符, 用于缓存已解析过的文件
const id = ID++;
// 该`presets`选项是一组规则,告诉`babel`如何传输我们的代码.
// 用`babel-preset-env`将代码转换为浏览器可以运行的东西.
const { code } = transformFromAst(ast, null, {
presets: ['env']
});
// 返回此模块的相关信息
return {
id, // 文件id(唯一)
filename, // 文件路径
dependencies, // 文件的依赖关系
code // 文件的代码
};
}
// 我们将提取它的每一个依赖文件的依赖关系,循环下去:找到对应这个项目的`依赖图`
function createGraph(entry) {
// 得到入口文件的依赖关系
const mainAsset = createAsset(entry);
const queue = [mainAsset];
for (const asset of queue) {
asset.mapping = {};
// 获取这个模块所在的目录
const dirname = path.dirname(asset.filename);
asset.dependencies.forEach((relativePath) => {
// 通过将相对路径与父资源目录的路径连接,将相对路径转变为绝对路径
// 每个文件的绝对路径是固定、唯一的
const absolutePath = path.join(dirname, relativePath);
// 递归解析其中所引入的其他资源
const child = createAsset(absolutePath);
asset.mapping[relativePath] = child.id;
// 将`child`推入队列, 通过递归实现了这样它的依赖关系解析
queue.push(child);
});
}
// queue这就是最终的依赖关系图谱
return queue;
}
// 自定义实现了require 方法,找到导出变量的引用逻辑
function bundle(graph) {
let modules = '';
graph.forEach((mod) => {
modules += `${mod.id}: [
function (require, module, exports) { ${mod.code} },
${JSON.stringify(mod.mapping)},
],`;
});
const result = `
(function(modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(name) {
return require(mapping[name]);
}
const module = { exports : {} };
fn(localRequire, module, module.exports);
return module.exports;
}
require(0);
})({${modules}})
`;
return result;
}
// ❤️ 项目的入口文件
const graph = createGraph('./example/entry.js');
const result = bundle(graph);
// ⬅️ 创建dist目录,将打包的内容写入main.js中
fs.mkdir('dist', (err) => {
if (!err)
fs.writeFile('dist/main.js', result, (err1) => {
if (!err1) console.log('打包成功');
});
});
1)从入口文件开始解析
2)查找入口文件引入了哪些js文件,找到依赖关系
3)递归遍历引入的其他js,生成最终的依赖关系图谱
4)同时将ES6语法转化成ES5
5)最终生成一个可以在浏览器加载执行的 js 文件
dist/main.js文件
// 文件里是一个立即执行函数
(function(modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(name) {
// ⬅️ 第四步 跳转到这里 此时mapping[name] = 1,继续执行require(1)
// ⬅️ 第六步 又跳转到这里 此时mapping[name] = 2,继续执行require(2)
return require(mapping[name]);
}
const module = { exports : {} };
// 第二步 ,执行fn函数
fn(localRequire, module, module.exports);
return module.exports;
}
// 第一步 执行require(0)
require(0);
})({
// 立即执行函数的参数是一个对象,该对象有3个属性
// 0 代表entry.js;
// 1 代表message.js
// 2 代表name.js
0: [
// 第三步 跳转到这里 继续执行require('./message.js')
function (require, module, exports) {
"use strict";
var _message = require("./message.js");
var _message2 = _interopRequireDefault(_message);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// 将message的内容显示到页面中
var p = document.createElement('p');
p.innerHTML = _message2.default;
document.body.appendChild(p);
},
{ "./message.js": 1 },
], 1: [
function (require, module, exports) {
"use strict";
// ⬅️ 第五步 跳转到这里 继续执行require('./name.js')
Object.defineProperty(exports, "__esModule", {
value: true
});
var _name = require("./name.js");
// ⬅️ 第八步 跳到这里 此时_name为{name: 'Webpack'}, 在exports对象上设置default属性,值为'hello Webpack!'
exports.default = "hello " + _name.name + "!";
},
{ "./name.js": 2 },
], 2: [
function (require, module, exports) {
"use strict";
// ⬅️ 第七步 跳到这里 在传入的exports对象上添加name属性,值为'Webpack'
Object.defineProperty(exports, "__esModule", {
value: true
});
var name = exports.name = 'Webpack';
},
{},
],
})
```js
分析文件的执行过程
1)整体大致分为10步,第一步从require(0)开始执行,调用内置的自定义require函数,跳转到第二步,执行fn函数
2)执行第三步require('./message.js'),继续跳转到第四步 require(mapping['./message.js']), 最终转化为require(1)
3)继续执行require(1),获取modules[1],也就是执行message.js的内容
4)第五步require('./name.js'),最终转化为require(2),执行name.js的内容
5)通过递归调用,将代码中导出的属性,放到exports对象中,一层层导出到最外层
6)最终通过_message2.default获取导出的值,页面显示hello Webpack!
Webpack的打包流程
总结一下webpack完整的打包流程
1)webpack从项目的entry入口文件开始递归分析,调用所有配置的 loader对模块进行编译
因为webpack默认只能识别js代码,所以如css文件、.vue结尾的文件,必须要通过对应的loader解析成js代码后,webpack才能识别
2)利用babel(babylon)将js代码转化为ast抽象语法树,然后通过babel-traverse对ast进行遍历
3)遍历的目的找到文件的import引用节点
因为现在我们引入文件都是通过import的方式引入,所以找到了import节点,就找到了文件的依赖关系
4)同时每个模块生成一个唯一的id,并将解析过的模块缓存起来,如果其他地方也引入该模块,就无需重新解析,最后根据依赖关系生成依赖图谱
5)递归遍历所有依赖图谱的模块,组装成一个个包含多个模块的 Chunk(块)
6)最后将生成的文件输出到 output 的目录中