手写webpack (2)
-
在m-pack 通过path 引入 webpack.config.js
let config = require(path.resolve('webpack.config.js')); console.log(config) 输出: { mode: 'development', entry: './src/index.js', output: { path: 'C:\\Users\\zxw\\Desktop\\js\\handwritten_webback\\webpack-dev\\dist', filename: 'bundle.js' } }
webpack.config.js
let path = require('path'); module.exports = { mode: 'development', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' } }
可以看出, config 是 webpack.config.js 里面导出的配置
2. 在bin 创建 Compiler.js 在 m-pack 引入 然后 通过 new 实例化引入的 Compiler 最后 通过 run 方法启动
2. 在bin 创建 Compiler.js 在 m-pack 引入 然后 通过 new 实例化引入的 Compiler 最后 通过 run 方法启动
#! /usr/bin/env node console.log('start 2020') let path = require('path') //配置文件 let config = require(path.resolve('webpack.config.js')); console.log(config) let Compiler = require("../bin/Compiler") let compiler = new Compiler(config); compiler.hooks.entryOption.call(); compiler.run()
3.编写 Compiler 这个文件
-
在constructor 接受传入的配置
-
this.entry 是传入配置的入口文件
-
this.config 将配置挂载到全局上
-
this.root = process.cwd(); 是项目的工作的目录 如: C:\Users\zxw\Desktop\js\handwritten_webback\webpack-dev
-
this.entryId 用来存放主入口
-
buildModules 通过入口文件的路径来 获取源码 构建依赖关系
-
emitFile 用来 最后解析好的源码和依赖关系发射出去
let path = require('path') class Compiler { constructor(config) { this.config = config // 需要保存的主入口文件路径 this.entryId; // './src/index.js' // modules对象用来存放依赖key和源码value this.modules = {}; // 入口路径 entry: './src/index.js', this.entry = config.entry; //工作路径 C:\Users\zxw\Desktop\js\handwritten_webback\webpack-dev this.root = process.cwd(); console.log(path.resolve(this.root, this.entry)) } run() { // C:\Users\zxw\Desktop\js\handwritten_webback\webpack-dev\src\index.js this.buildModules(path.resolve(this.root, this.entry), true); // 发射一个文件 this.emitFile(); } buildModules(modulePath, isEntry) { } // 发射文件 emitFile() { } } module.exports = Compiler;
-
buildModules 详解
-
通过 getSource 拿到模块的内容
let source = this.getSource(modulePath) // let str = require('./a.js') // console.log(str)
-
获取模块的入口
let moduleName = './' + path.relative(this.root, modulePath) console.log(moduleName) ./src\index.js
-
判断如果是入口文件 就将它赋值给this.entryId 如果不是 进行 解析
-
调用 parse 方法进行解析 传入 模块的内容 和 父文件 的目录
path.dirname(moduleName) 输出为 ./src
parse()方法返回了源码sourceCode和依赖dependencies
buildModules(modulePath, isEntry) { console.log(111) // 拿到模块的内容 let source = this.getSource(modulePath) // console.log(source) //./src\index.js let moduleName = './' + path.relative(this.root, modulePath) // console.log(moduleName) if (isEntry) { // 保存入口 this.entryId = moduleName } console.log(path.dirname(moduleName)) // 解析 需要把source 源码解析 let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)) }
5. parse 方法
parse(source, parentPath) { let dependencies = []; let ast = babylon.parse(source); traverse(ast, { CallExpression(p) { if (p.node.callee.name === 'require') { p.node.callee.name = '__webpack_require__'; let moduleName = p.node.arguments[0].value; moduleName = moduleName + (path.extname(moduleName) ? '' : '.js'); moduleName = './' + path.join(parentPath, moduleName); dependencies.push(moduleName); p.node.arguments = [t.stringLiteral(moduleName)] } } }); let sourceCode = generator(ast).code; return { sourceCode, dependencies }; }
引入 @babel/traverse @babel/types @babel/generator 这几个创建解析 ast
npm i babylon @babel/traverse @babel/types @babel/generator -D
首先,parse()方法是解析模块的依赖关系的。它接收了两个参数,分别是源码source和父级路径。方法内部定义了一个空数组dependencies,用来存放解析到的依赖。接着通过第三方库Babylon生成该源码的抽象语法树ast,又通过traverse解析该语法树,解析语法树的流程为: 首先找到节点node的callee,callee有个name属性。找到name为require即找到该文件的依赖,并'require'其替换为'__webpack_require__',至于为什么替换为这个名字,原因是webpack打包后自己实现的require方式名就是'__webpack_require__'。替换后方便我们之后进行模版操作。
紧接着,又拿到了依赖的文件名a.js,不过这个依赖的文件名不一定都是带有后缀.js的。所以,判断它有没有后缀名,没有的话手动添加.js。
接下来就用到了刚才传递的第二个参数:父级路径'./src'。拿到依赖文件名'a.js'和父级路径'./src',将其拼接成依赖的相对路径'./src/a.js'。并把该依赖存到dependencies依赖数组中。
并且通过babel-types将ast中的把AST中的stringLiteral替换为相对路径。
最后一步,通过替换好的抽象语法树和babel-generator,生成结果。并返回souceCode和dependencies。
在buildModule方法中打印this.module即可看到结果:
{ './src\\index.js': 'let str = __webpack_require__("./src\\\\a.js");\n\nconsole.log(str);' }
可以看到,this.module对象中存放了key(依赖的路径)和value(ast语法树解析后的源码)。表示已经成功解析。但是value中还是有__webpack_require__,因为刚才只解析一次,我们需要递归解析所有的依赖,并放到this.module中。
递归解析依赖很简单,只需要循环解析dependencies数组的元素即可,因为dependencies数组中存放的便是所有依赖。
注意,第二个参数要传fasle。因为主入口路径只有一个。执行第一次该this.buildModule()方法就已经把主入口确定并且保存到this.entryId中。
buildModules(modulePath, isEntry) { console.log(111) // 拿到模块的内容 let source = this.getSource(modulePath) // console.log(source) //./src\index.js let moduleName = './' + path.relative(this.root, modulePath) // console.log(moduleName) if (isEntry) { // 保存入口 this.entryId = moduleName } // 解析 需要把source 源码解析 let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)) this.modules[moduleName] = sourceCode dependencies.forEach((depend) => { this.buildModules(path.join(this.root, depend), false); }); console.log(this.modules); // console.log(path.dirname(moduleName)) }
输出:
{ './src\\index.js': 'let str = __webpack_require__("./src\\\\a.js");\n\nconsole.log(str);', './src\\a.js': 'let b = __webpack_require__("./src\\\\base\\\\b.js");\n\nmodule.exports = b + \'2020\';', './src\\base\\b.js': 'module.exports = \'您好a\';' } { './src\\index.js': 'let str = __webpack_require__("./src\\\\a.js");\n\nconsole.log(str);', './src\\a.js': 'let b = __webpack_require__("./src\\\\base\\\\b.js");\n\nmodule.exports = b + \'2020\';', './src\\base\\b.js': 'module.exports = \'您好a\';' } { './src\\index.js': 'let str = __webpack_require__("./src\\\\a.js");\n\nconsole.log(str);', './src\\a.js': 'let b = __webpack_require__("./src\\\\base\\\\b.js");\n\nmodule.exports = b + \'2020\';', './src\\base\\b.js': 'module.exports = \'您好a\';' }
所有的依赖关系,index.js, a.js, .js 都被完整的保存到this.modules对象中。
笔记:
https://app.yinxiang.com/fx/9f6e2d40-c0b8-4cf6-9366-c091db2446e1
代码:
越努力越幸运