1、新建一个项目(搭建架子)
npm init - y
需要以下几个文件
1、webpack.config.js 作为 options
const path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js', // 需要打包的文件
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js',
},
}
2、lib/webpack.js // 作为webpack的架子框架来扩展
const fs = require('fs')
module.exports = class webpack {
constructor(options) {
const { entry, output } = options
this.entry = entry
this.output = output
}
run() {
this.parse(this.entry) // 输出入口
}
parse(entryFile) {
const content = fs.readFileSync(entryFile, 'utf-8')
console.log(content) // 输出原本内容
}
}
3、bundle.js 通过此文件运行打包
const options = require('./webpack.config.js')
const Webpack = require('./lib/webpack')
new Webpack(options).run()
4、准备需要的打包文件
src/index.js
import { fuck } from './a.js'
console.log(fuck)
console.log('webpack -4.0xxxx!!!')
src/a.js
export const fuck = "fuck you"
通过上述代码我们就可以搭出一个webpack的架子
一个webpack的类,一份webpack的配置,需要打包的文件,还有执行打包的代码
我们将流程分为3部分
第一部分: parse过程 主要读取打包文件内容,通过babel库我们解析每个文件的导入语句,我们获取每个文件的加载映射
比如 main.js 文件 中 import a.js 还有 b.js 文件,那么我们要生成 一个依赖映射
用一个对象保存
{
'main.js': {
yilai: {
'./a.js' : '这里存相对入口的路径,
'./b.js' : '这里存相对入口的路径'
}
}
}
第二部分:traverse 转义代码, 生成浏览器可执行的文件内容。把 import 转化为 require 的语法
第三部分: 生成打包后的文件, 主要处理 require 函数加载其他文件的函数
2、首先目标是扩展webpack类 的 parse 函数
class webpack {
// ...
parse(entryFile) {
const content = fs.readFileSync(entryFile, 'utf-8')
console.log(content) // 输出原本内容
}
}
对代码解析第一件事:
1、转义成ast 树
我们引用 @babel/parse ,我们可以通过它来转义成 ast
const parser = require('@babel/parser')
// content 读取到的文件内容
const ast = parser.parse(content, {
sourceType: 'module', // es6
})
ast 的内容是一个node类型的对象
{
type: 'File',
start: 0,
end: 86,
loc:
SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 4, column: 0 },
filename: undefined,
identifierName: undefined },
// ...
program: // 第一层 关注这里
{
type: 'Program',
start: 0,
end: 86,
//....
body: [ [Node], [Node], [Node] ], // 第二层 关注这里
}
}
ast.program.body 数组内的对象是行对代码的解析
有type: 'ImportDeclaration', 导入语句 表达式等等
对代码解析成第二件事:
2、traverse 对代码转换过程
traverse(ast, null, options) 可以找到导入语句
console.log('traverse----------:')
const traverse = require('@babel/traverse').default
const yilai = {}
const prePath = path.dirname(entryFile)
traverse(ast, {
ImportDeclaration({ node }) {
console.log(node) // 过滤出来 type 为 ImportDeclaration 节点
// 要的 node.source.value
// 对路径做拼接
const newPath = './' + path.join(prePath, node.source.value)
yilai[node.source.value] = newPath.replace('\\', '/') // 收集路径
},
})
console.log('yilai:' + yilai) // {'./a.js': './src/a.js'} 得到相对于入口文件的路径
对代码解析成第三件事:
2、把ast 解析成想要得代码
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env'],
})
console.log('code:-----------')
console.log(code)
return {
entryFile,
yilai,
code,
}
此时转义后的代码会成这样
var _a = require("./a.js"); // 这块 语法浏览器 还是解析不了,毕竟是node的
console.log(_a.fuck);
console.log('webpack -4.0xxxx!!!');
汇总后的代码:
class webpack {
// ...
parse(entryFile) {
const content = fs.readFileSync(entryFile, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module', // es6
})
// 实际要的是 ast.program.body ===> [ [Node], [Node], [Node] ] 行代码
const yilai = {}
const prePath = path.dirname(entryFile)
traverse(ast, {
ImportDeclaration({ node }) {
console.log(node) // 过滤出来 type 为 ImportDeclaration 节点
// 要的 node.source.value
// 对路径做拼接
const newPath = './' + path.join(prePath, node.source.value)
yilai[node.source.value] = newPath.replace('\\', '/')
},
})
// 得到编译后的代码
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env'],
})
// 返回
return {
entryFile,
yilai,
code,
}
}
}
3、扩展 generateCode 函数
首先我们得明白run做了什么
run() {
const info = this.parse(this.entry)
this.modules.push(info)
for (let i = 0; i < this.modules.length; i++) {
const item = this.modules[i]
const { yilai } = item
if (yilai) {
for (let j in yilai) {
this.modules.push(this.parse(yilai[j]))
}
}
}
const obj = {}
this.modules.forEach((item) => {
obj[item.entryFile] = item
})
console.log(obj) // 此时获得了 图谱
// 通过parse 形成 映射
// 对象中的key 是 相对入口的路径
// value: {
// yilai: 文件加载 url到 相对入口文件的路径的映射
// code:'真实代码的字符串'
// }
this.gennerateCode(obj) // 生成代码和创建bundle文件
}
gennerateCode 实现
const filePath = path.join(this.output.path, this.output.filename) // 输出文件目录和名
const content = `(function (modules) {
// 加载函数
function require(module) {
function newRequire(relativePath) {
// .a.js => ./src/a.js
return require(modules[module].yilai[relativePath])
}
const exports = {}; // 这个分号缺了就报错了,会和下面自执行函数连起来报错
// require 重载,这样加载的url就是对的了
(function(require, code) {
eval(code)
})(newRequire, modules[module].code)
return exports
}
return require('${this.entry}') /* ./src/index.js */
})(${JSON.stringify(obj)})`
fs.writeFileSync(filePath, content, 'utf-8')
完整代码清单
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const path = require('path')
const fs = require('fs')
const { transformFromAst } = require('@babel/core')
module.exports = class webpack {
constructor(options) {
const { entry, output } = options
this.entry = entry
this.output = output
this.dir = path.dirname(this.entry)
this.modules = []
}
run() {
const info = this.parse(this.entry)
this.modules.push(info)
for (let i = 0; i < this.modules.length; i++) {
const item = this.modules[i]
const { yilai } = item
if (yilai) {
for (let j in yilai) {
this.modules.push(this.parse(yilai[j]))
}
}
}
const obj = {}
this.modules.forEach((item) => {
obj[item.entryFile] = item
})
console.log(obj) // 此时获得了 图谱
this.gennerateCode(obj) // 生成代码和创建bundle文件
}
parse(entryFile) {
const content = fs.readFileSync(entryFile, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module', // es6
})
// 实际要的是 ast.program.body ===> [ [Node], [Node], [Node] ] 行代码
const yilai = {}
const prePath = path.dirname(entryFile)
const currentPath = traverse(ast, {
ImportDeclaration({ node }) {
// console.log(node) // 过滤出来 type 为 ImportDeclaration 节点
// 要的 node.source.value
// 对路径做拼接
const newPath = './' + path.join(prePath, node.source.value)
yilai[node.source.value] = newPath.replace(/\\/g, '/')
},
})
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env'],
})
return {
entryFile,
yilai,
code,
}
}
gennerateCode(obj) {
const filePath = path.join(this.output.path, this.output.filename) // 输出文件目录和名
const content = `(function (modules) {
// 加载函数
function require(module) {
function newRequire(relativePath) {
// .a.js => ./src/a.js
return require(modules[module].yilai[relativePath])
}
const exports = {};
(function(require, code) {
eval(code)
})(newRequire, modules[module].code)
return exports
}
return require('${this.entry}') /* ./src/index.js */
})(${JSON.stringify(obj)})`
fs.writeFileSync(filePath, content, 'utf-8')
}
}
最后代码验证:
node bundle.js
结果生成文件 main.js
;(function (modules) {
// 加载函数
function require(module) {
function newRequire(relativePath) {
// .a.js => ./src/a.js
return require(modules[module].yilai[relativePath])
}
const exports = {};
(function (require, code) {
eval(code)
})(newRequire, modules[module].code)
return exports
}
return require('./src/index.js') /* ./src/index.js */
})({
'./src/index.js': {
entryFile: './src/index.js',
yilai: { './a.js': './src/a.js' },
code:
'"use strict";\n\nvar _a = require("./a.js");\n\nconsole.log(_a.fuck);\nconsole.log(\'webpack -4.0xxxx!!!\');',
},
'./src/a.js': {
entryFile: './src/a.js',
yilai: { './file/b.js': './src/file/b.js' },
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.fuck = void 0;\n\nvar _b = require("./file/b.js");\n\nconsole.log(_b.hello);\nvar fuck = \'fuck you\';\nexports.fuck = fuck;',
},
'./src/file/b.js': {
entryFile: './src/file/b.js',
yilai: {},
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.hello = void 0;\nvar hello = \'hello world\';\nexports.hello = hello;',
},
})