初步了解下Webpack是什么?它到底有什么作用?
什么是Webpack?
- webpack 是一个现代的 javascript应用程序的静态打包工器(modules bundle) 当webpack处理应用程序的时候 会递归生成一个模块关系图(dependency graph) 其中包含应用程序需要的每一个模块,然后将这些模块打包成一个或多个bundle文件
- webpack就像一个生产线,要经过一些列的处理流程后才能将源文件转换成输出结果,在这条生产线上每个处理流程的指责都是单一的并且多流程之间存在互相依赖的关系,只有完成当前的处理才可以交给下一个流程处理,而插件就像是一个插入生产线上的一个功能 在特定的时机(广播事件模式)对生产线上的资源做处理
- wevpack 通过事TapAble来组织这条组杂的生产线 webpack在运行期间会通过广播事件 每一个插件只关心它自己监听的事件 就可以添加到这个生产线中 从而改变生产线的运作 webpack 的事件流机制会保证了插件的有序性 使整个系统扩展性性(插件/新增插件)能变得更好
webpack 的作用
- 模块打包 :可以将不同模块的文件打包整合在一起 并且保证他们之间的引用正确 执行有序 利用打包我们就可以在开发的时候根据我们的业务自由划分文件模块 保证项目结构的清晰和可读性
- 编译兼容 在前端的上古时期 手写一堆浏览器兼容代码一直都是令前端工程师头皮发麻的事情 而在今天这个问题被大大弱化了 通过webpack的loader机制 不仅可以帮我们对代码做polyfill 还可以编译转换 如.lessm.vue,.jsx 这类在浏览器无法识别的格式文件 让我们在开发时候使用新特性和新语法开发 提高开发效率
- 能力扩展 通过webpack的plugin机制 我们在实现模块化打包和编译兼容的基础上 可以进一步实现诸如按需加载 代码压缩等一系列功能 帮助我们进一步提高自动化程度 工程效率以及打包输出质量
webpack构建流程
webpack 的运行流程是一个单一串行的过程,从启动到结束如上图一下
- 首先会从配置文件(webpack.config.js)和Shell(package.josn文件中)语句中读取与合并参数 初始化需要的插件和配置插件等执行环境的参数;初始化后 将会调用Compiler(类)中的run来真正启动webpack编译构建过程,webpack的构建过程包括 compiler make build seal emit阶段
- 从配置文件和shell中获取合并参数 得到最终参数
- 用compiler获取到上一步初始化的Compiler对象(第一次自己初始化) 加载所有配置的插件 并且执行对象的run方法 开始编译
- 根据配置文件中entry找到所有入口文件
- make 编译模块: 从入口文件开始出发 调用所有配置的loader 对模块进行编译再找出模版依赖的子模块 再递归本步骤 直到所有入口上的依赖都经历了本步骤的处理
- build module 完成模块编译:经过loader转移完所有模块后 得到了每个模块编译以后的最终内容以及他们之间的依赖关系
- seal 输出资源 根据入口文件和模块之间的依赖关系 组成一个或多个的Chunk 再把每个chunk转换成一个单的文件加入输出列表中 (这一步事最后可以修改输出内容的地方)
- emit 输出完成:在确定好输出内容后 根据配置的输出路径和输出文件名 把内容写进文件系统中
总结:
- 初始化 :启动构建 读取与合并的配置参数 加载Plugin 实例化Compiler
- 编译:从入口Entry出发 针对每一个Module串行调用对应的loader去转移文件的内容 再从该Module去寻找子Module 递归的去处理
- 输出:将编译后的Module组成Chunk(代码快,一个Chunk由多个模块组合而成,用于代码合并与切割)转换成文件 输出到文件系统中
根据上述内容 SuperWebpack 开始准备工作
目录的结构
|-- forestpack
|-- dist
| |-- bundle.js
| |-- index.html
|-- lib
| |-- compiler.js
| |-- index.js
| |-- parser.js
| |-- test.js
|-- src
| |-- greeting.js
| |-- index.js
|-- superWebpack.config.js
|-- package.json
- dist 打包后的出入文件
- lib 核心文件 主要是Compiler和parse
-
- compiler 主要处理相关编译 compiler作为一个类 他内部的run方法去开启编译,还有构建module(buildModule)和输出文件(emitFiles)
-
- parser 主要是用于解析相关,包含解析AST(getAst)收集依赖(getDependencies),替换(es6转换es5)
-
- index 主要用于实例化compiler类 并且配置参数传入传入
-
- text 用于测试文件
- src 业务代码
- superWebpack.config.js 配置文件
首先维护 superWebpack代码 也就是默认配置文件
const path = require("path"); module.exports = { entry: path.join(__dirname, "./src/index.js"), output: { path: path.join(__dirname, "./dist"), filename: "bundle.js", }, };
src下面的greeting.js和index.js文件
// greeting.js
export function greeting(name) { return "你好" + name; }
// index
import { greeting } from "./greeting.js"; document.write(greeting("森林"));
compile.js编写
const path = require("path");
const fs = require("fs");
module.exports = class Compiler {
// 接收通过lib/index.js new Compiler(options).run()传入的参数,对应`subpack.config.js`的配置
constructor(options) {
const { entry, output } = options;
this.entry = entry;
this.output = output;
this.modules = [];
}
// 开启编译
run() {}
// 构建模块相关
buildModule(filename, isEntry) {
// filename: 文件名称
// isEntry: 是否是入口文件
}
// 输出文件
emitFiles() {}
};
compile.js主要做了这几个事情
- 接收subwebpack参数 并且给entry 和 output初始化赋值
- 开始编译run方法 处理构建模块 ,收集依赖,输出文件
- buildModule 主要用于构建模块(被run调用)
- emitFiles 输出文件(同样被run调用)
parser.js编写
const fs = require('fs');
/**
* 获取将源码生成AST的函数 由babel包提供的一个方法
* parms:{
* sring
* }
* */
const parser = require('@babel/parser');
/**
* 对AST节点进行递归便利
* */
const traverse = require('@babel/traverse').default;
/**
* 将获取的ES6的AST转换成ES5代码 的一个函数
* */
const { transformFromAst } = require('babel-core');
const { parse } = require('path');
module.exports = {
// 解析我们的代码生成AST抽象语法树
getAST: path => {
// 通过同步读取文件 传入我们的文件路径 以及转换的格式
const source = fs.readFileSync(path, 'utf-8');
return parser.parse(source, {
// 设置我们要解析的是ES模块
sourceType: 'module',
});
},
// 对AST节点进行递归便利 (就是处理每一个模块的代码)
getDependencies: ast => {
const dependencies = [];
traverse(ast, {
ImportDeclaration: ({ node }) => {
dependencies.push(node, source.value);
},
});
return dependencies
},
// 将获得的ES6的AST转换成ES5
transform: ast => {
const { code } = transformFromAst(ast, null, {
presets: ['env'],
});
return code;
},
};
完善compile.js
const { getAST, getDependencies, transform } = require('./parser');
const path = require('path');
const fs = require('fs');
module.exports = class Compiler {
// 接受传入的配置参数 为option 对应option=superWebpack.config.js
constructor(option) {
const { entry, output } = option;
this.entry = entry;
this.output = output;
this.module = [];
}
// 构造开始编译函数 处理构建模块(buildModule) 收集依赖 输出文件(emitFiles)等
run() {
const entryModule = this.buildModule(this.entry, true);
this.modules.push(entryModule);
this.modules.map(_module => {
_module.dependencies.map(dependency => {
this.modules.push(this.buildModule(dependency));
});
});
this.emitFiles();
}
// 构建模块相关函数 提供给run使用
buildModule(filename, isEntry) {
// filename 传入的文件名
// 是否为入口文件
let ast;
if (isEntry) {
ast = getAST(filename);
} else {
const absolutaPath = path.join(process.cwd(), '../src', filename);
ast = getAST(absolutaPath);
}
return {
filename, // 文件名称
dependencies: getDependencies(ast), // 依赖的列表
transformCode: transform(ast), // 转换后的代码
};
}
// 输入文件函数 提供给run使用
emitFiles() {
// 首先获取到打包之后的路径和文件名
const outputPath = path.join(this.output.path, this.output.filename);
// 创建好需要打包的模块初始函数
let modules = '';
// 通过上述的整合好的代码数组 依次加入modules
this.modules.map(_module => {
modules += `'${_module.filename}' : function(require, module, exports) {${_module.transformCode}},`;
});
const bundle = `
(function(modules) {
function require(fileName) {
const fn = modules[fileName];
const module = { exports:{}};
fn(require, module, module.exports)
return module.exports
}
require('${this.entry}')
})({${modules}})
`;
// 写入文件
fs.writeFileSync(outputPath, bundle, 'utf-8');
}
};
/***
* webpack原版打包后的文件 (精简过的)
* webpake 将所有的模块(文件)包裹在一个函数中 并传入默认参数 将所有的模块放入一个数组中 取名为modules
* 并通过数组的下角标作为moduleId
* webpack 将所有模块(可以简单理解成文件)包裹于一个函数中,并传入默认参数,将所有模块放入一个数组中,取名为 modules,并通过数组的下标来作为 moduleId。
将 modules 传入一个自执行函数中,自执行函数中包含一个 installedModules 已经加载过的模块和一个模块加载函数,最后加载入口模块并返回。
__webpack_require__ 模块加载,先判断 installedModules 是否已加载,加载过了就直接返回 exports 数据,没有加载过该模块就通过 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) 执行模块并且将 module.exports 给返回。
*
*/
/**
* dist/index.xxxx.js
(function(modules) {
// 已经加载过的模块
* 将modules传入一个自执行函数中 自执行函数中包含一个installedModules已经加载过的模块
* 和一个模块加载函数 最后加载入口并返回
*
var installedModules = {};
// 模块加载函数
function __webpack_require__(moduleId) {
* 先判断installedModules是否已经被加载过了 如果加载过了直接返回exports数据
* 没有加载过 就通过modules[moduleId].call(module.exports, module, module.exports, __webpack_require__) 执行模块并且将 module.exports 给返回
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
__webpack_require__(0);
})(
[
/* 0 module
(function(module, exports, __webpack_require__) {
...
}),
/* 1 module
(function(module, exports, __webpack_require__) {
...
}),
/* n module
(function(module, exports, __webpack_require__) {
...
})]
);
**/
最后编译完成打包的文件代码
(function (modules) {
function require(fileName) {
const fn = modules[fileName];
const module = { exports: {} };
fn(require, module, module.exports);
return module.exports;
}
require("/Users/fengshuan/Desktop/workspace/forestpack/src/index.js");
})({
"/Users/fengshuan/Desktop/workspace/forestpack/src/index.js": function (
require,
module,
exports
) {
"use strict";
var _greeting = require("./greeting.js");
document.write((0, _greeting.greeting)("森林"));
},
"./greeting.js": function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.greeting = greeting;
function greeting(name) {
return "你好" + name;
}
},
});