一句话:从入口文件开始,对引用的依赖进行收集处理,最后输出到指定目录中。
- webpack是一个帮助开发者自动构建模块的工具。
- 给webpack一个入口模块路径,还你一个最终文件。其中入口模块可以依赖其他模块,其他模块又可以依赖另外的模块。
- webpack会自动处理模块之间的依赖关系
从执行一个命令说起:npm run build,当执行该命令时,会找到package.json中的脚本命令,可以看到会使用node命令执行./script/build.js中的代码。
// package.json
"scripts": {
"build": "node ./script/build.js"
},
将配置文件和打包器(webpack,其本质是个函数,接收配置对象为参数)整合
// 自己写的webpack
const myWebpack = require('../lib/myWebpack')
// webpack配置文件
const config = require('../config/webpack.config')
// 根据配置文件(配置对象),获取编译器 (函数)
let compiler = myWebpack(config)
// 调用编译器对象的执行接口,开始打包编译
compiler.run()
webpack的配置文件
// webpack.config.js
const {resolve} = require('path')
module.exports = {
// 打包入口配置,指定入口文件的路径
entry: './src/index.js',
// 打包输出配置
output: {
// 指定输出文件的目录
path: resolve(__dirname, '../dist'),
// 指定输出文件的名称
filename: 'main.js'
}
}
自己写的打包器(webpack)的主文件
// webpack的入口文件
// 引入编译器
const Compiler = require('./Compiler')
// 导出打包工具(webpack),就是个函数
module.exports = function myWebpack(config){
// 返回一个编译器对象,接收配置对象
return new Compiler(config)
}
编译器
const path = require('path');
const fs = require('fs');
// 自定义模块
const {getAst, getDeps, getCode} = require('./parser')
// 定义并导出编译器
module.exports = class Compiler{
// 构造函数
constructor(options = {}){
// 接收配置对象
this.options = options
// 所有依赖的容器
/*
[
{
code: '',
deps: {},
entryFilePath: ''
}
]
*/
this.modules = [];
}
// 入口方法
run(){
// 获取入口文件路径
const entryFilePath = this.options.entry;
// 第一次构建,得到入口文件的信息
const fileInfo = this.build(entryFilePath);
// 将入口文件的信息放到依赖容器中,此时依赖容器中存放着一个入口文件的信息
this.modules.push(fileInfo);
// 递归收集依赖
this.getDepsMethod(fileInfo.deps);
// 将 this.modules 转换成 depsGraph
const depsGraph = this.modules.reduce((graph, module) => {
graph[module['entryFilePath']] = {
code: module.code,
deps: module.deps,
}
return graph
}, {})
this.generate(depsGraph)
}
// 递归收集依赖
getDepsMethod(deps) {
for (const key in deps) {
// 是否存在该依赖
let item = this.modules.find((item) => item.entryFilePath == deps[key])
// 存在则跳过本次遍历,进行下一次遍历
if(item) continue;
// 获取该依赖的信息
const fileInfo = this.build(deps[key])
// 将依赖存到容器中
this.modules.push(fileInfo)
// 该依赖如果还有依赖则继续依赖收集
if(JSON.stringify(fileInfo.deps) != '{}'){
this.getDepsMethod(fileInfo.deps)
}
}
}
// 生成输出资源
generate(depsGraph){
const bundle = `
(function (depsGraph) {
// require目的:为了加载入口文件
function require(module) {
// 定义模块内部的require函数
function localRequire(relativePath) {
// 为了找到要引入模块的绝对路径,通过require加载
return require(depsGraph[module].deps[relativePath]);
}
// 定义暴露对象(将来我们模块要暴露的内容)
var exports = {};
(function (require, exports, code) {
eval(code);
})(localRequire, exports, depsGraph[module].code);
// 作为require函数的返回值返回出去
// 后面的require函数能得到暴露的内容
return exports;
}
// 加载入口文件
require('${this.options.entry}');
})(${JSON.stringify(depsGraph)})
`
// 生成输出文件的绝对路径
const outputFilePath = path.resolve(this.options.output.path, this.options.output.filename)
// 写入文件
fs.writeFileSync(outputFilePath, bundle, 'utf-8');
}
// 构建方法
build(entryFilePath){
// 1. 将文件解析成ast
const ast = getAst(entryFilePath);
// 2. 获取ast中所有的依赖
const deps = getDeps(entryFilePath, ast);
// 3. 将ast解析成code
const code = getCode(ast);
return {
// 文件路径
entryFilePath,
// 当前文件的所有依赖
deps,
// 当前文件解析后的代码
code
}
}
}
解析工具
- 根据读取的文件模块内容生成语法树;
- 根据语法树获取该模块的依赖;
- 根据语法树获取需要执行的代码;
// 内置模块
const fs = require('fs');
const path = require('path');
// 第三方模块
const babelParser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');
module.exports = {
// 获取语法树
getAst(entryFilePath){
// 根据文件路径,同步读取文件内容
const fileContent = fs.readFileSync(entryFilePath, 'utf-8');
// 使用@babel/parser,将文件内容转换成AST语法树
const ast = babelParser.parse(fileContent, {
sourceType: 'module' // 解析文件的模块化方案是 ES Module
})
return ast
},
// 获取依赖
getDeps(entryFilePath, ast){
// 获取路径中最后文件或目录的目录,'./src/index.js' ——> './src'
const dirname = path.dirname(entryFilePath);
// 定义存储依赖的容器
const deps = {}
// 使用traverse,收集依赖
traverse(ast, {
// 内部会遍历ast中program.body,判断里面语句类型
// 如果 type:ImportDeclaration 就会触发当前函数
ImportDeclaration({node}) {
// 文件相对路径:'./add.js'
const relativePath = node.source.value;
// 生成基于入口文件的绝对路径,将导入模块的路径转换成绝对路径
const absolutePath = path.resolve(dirname, relativePath);
// 添加依赖
/*
{
'./modules/add.js': "D:\myWebpack\src\modules\add.js",
'./modules/sub.js': "D:\myWebpack\src\modules\sub.js",
}
*/
deps[relativePath] = absolutePath;
}
})
return deps
},
// 获取代码
getCode(ast){
// 根据生成的语法树,通过transformFromAst工具和@babel/preset-env,将该模块解析生成低版本的代码(兼容性较好的代码,把let、const、箭头函数解析成var和function)
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
},
}