咱们先建一个项目html
项目目录暂时以下:node
已经把项目放到 github:https://github.com/Sunny-lucking/howToBuildMyWebpack 能够卑微地要个star吗webpack
咱们建立了add.js文件和minus.js文件,而后 在index.js中引入,再将index.js文件引入index.html。git
代码以下:es6
add.jsgithub
export default (a,b)=>{
return a+b; } 复制代码
minus.jsweb
export const minus = (a,b)=>{
return a-b } 复制代码
index.jsnpm
import add from "./add.js"
import {minus} from "./minus.js"; const sum = add(1,2); const division = minus(2,1); console.log(sum); console.log(division); 复制代码
index.html数组
<!DOCTYPE html>
<html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <script src="./src/index.js"></script> </body> </html> 复制代码
如今咱们打开index.html。你猜会发生什么???显然会报错,由于浏览器还不能识别import等ES6语法浏览器
不过不要紧,由于咱们原本就是要来解决这些问题的。
好了,如今咱们开始根据上面核心打包原理的思路来实践一下,第一步就是 实现获取主模块内容。
咱们来建立一个bundle.js文件。
// 获取主入口文件
const fs = require('fs') const getModuleInfo = (file)=>{ const body = fs.readFileSync(file,'utf-8') console.log(body); } getModuleInfo("./src/index.js") 复制代码
目前项目目录以下
咱们来执行一下bundle.js,看看是否成功得到入口文件内容
哇塞,不出所料的成功。一切尽在掌握之中。好了,已经实现第一步了,且让我看看第二步是要干吗。
哦?是分析模块了
分析模块的主要任务是 将获取到的模块内容 解析成AST语法树,这个须要用到一个依赖包@babel/parser
npm install @babel/parser
复制代码
ok,安装完成咱们将@babel/parser引入bundle.js,
// 获取主入口文件
const fs = require('fs') const parser = require('@babel/parser') const getModuleInfo = (file)=>{ const body = fs.readFileSync(file,'utf-8') // 新增代码 const ast = parser.parse(body,{ sourceType:'module' //表示咱们要解析的是ES模块 }); console.log(ast); } getModuleInfo("./src/index.js") 复制代码
咱们去看下@babel/parser的文档:
可见提供了三个API,而咱们目前用到的是parse这个API。
它的主要做用是 parses the provided code as an entire ECMAScript program,也就是将咱们提供的代码解析成完整的ECMAScript代码的AST。
再看看该API提供的参数
咱们暂时用到的是sourceType,也就是用来指明咱们要解析的代码是什么模块。
好了,如今咱们来执行一下 bundle.js,看看AST是否成功生成。
成功。又是不出所料的成功。
不过,咱们须要知道的是,当前咱们解析出来的不仅仅是index.js文件里的内容,它也包括了文件的其余信息。 而它的内容实际上是它的属性program里的body里。如图所示
咱们能够改为打印ast.program.body看看
// 获取主入口文件
const fs = require('fs') const parser = require('@babel/parser') const getModuleInfo = (file)=>{ const body = fs.readFileSync(file,'utf-8') const ast = parser.parse(body,{ sourceType:'module' //表示咱们要解析的是ES模块 }); console.log(ast.program.body); } getModuleInfo("./src/index.js" 复制代码
执行
看,如今打印出来的就是 index.js文件里的内容(也就是咱们再index.js里写的代码啦).
如今咱们须要 遍历AST,将用到的依赖收集起来。什么意思呢?其实就是将用import语句引入的文件路径收集起来。咱们将收集起来的路径放到deps里。
前面咱们提到过,遍历AST要用到@babel/traverse依赖包
npm install @babel/traverse
复制代码
如今,咱们引入。
const fs = require('fs')
const path = require('path') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const getModuleInfo = (file)=>{ const body = fs.readFileSync(file,'utf-8') const ast = parser.parse(body,{ sourceType:'module' //表示咱们要解析的是ES模块 }); // 新增代码 const deps = {} traverse(ast,{ ImportDeclaration({node}){ const dirname = path.dirname(file) const abspath = './' + path.join(dirname,node.source.value) deps[node.source.value] = abspath } }) console.log(deps); } getModuleInfo("./src/index.js") 复制代码
咱们来看下官方文档对@babel/traverse的描述
好吧,如此简略
不过咱们不难看出,第一个参数就是AST。第二个参数就是配置对象
咱们看看咱们写的代码
traverse(ast,{
ImportDeclaration({node}){ const dirname = path.dirname(file) const abspath = './' + path.join(dirname,node.source.value) deps[node.source.value] = abspath } }) 复制代码
配置对象里,咱们配置了ImportDeclaration方法,这是什么意思呢? 咱们看看以前打印出来的AST。
ImportDeclaration方法表明的是对type类型为ImportDeclaration的节点的处理。
这里咱们得到了该节点中source的value,也就是node.source.value,
这里的value指的是什么意思呢?其实就是import的值,能够看咱们的index.js的代码。
import add from "./add.js"
import {minus} from "./minus.js"; const sum = add(1,2); const division = minus(2,1); console.log(sum); console.log(division); 复制代码
可见,value指的就是import后面的 './add.js' 和 './minus.js'
而后咱们将file目录路径跟得到的value值拼接起来保存到deps里,美其名曰:收集依赖。
ok,这个操做就结束了,执行看看收集成功了没?
oh my god。又成功了。
如今咱们须要把得到的ES6的AST转化成ES5,前面讲到过,执行这一步须要两个依赖包
npm install @babel/core @babel/preset-env
复制代码
咱们如今将依赖引入并使用
const fs = require('fs')
const path = require('path') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const babel = require('@babel/core') const getModuleInfo = (file)=>{ const body = fs.readFileSync(file,'utf-8') const ast = parser.parse(body,{ sourceType:'module' //表示咱们要解析的是ES模块 }); const deps = {} traverse(ast,{ ImportDeclaration({node}){ const dirname = path.dirname(file) const abspath = "./" + path.join(dirname,node.source.value) deps[node.source.value] = abspath } }) 新增代码 const {code} = babel.transformFromAst(ast,null,{ presets:["@babel/preset-env"] }) console.log(code); } getModuleInfo("./src/index.js") 复制代码
咱们看看官网文档对@babel/core 的transformFromAst的介绍
害,又是一如既往的简略。。。
简单说一下,其实就是将咱们传入的AST转化成咱们在第三个参数里配置的模块类型。
好了,如今咱们来执行一下,看看结果
个人天,一如既往的成功。可见 它将咱们写const 转化成var了。
好了,这一步到此结束,咦,你可能会有疑问,上一步的收集依赖在这里怎么没啥关系啊,确实如此。收集依赖是为了下面进行的递归操做。
通过上面的过程,如今咱们知道getModuleInfo是用来获取一个模块的内容,不过咱们还没把获取的内容return出来,所以,更改下getModuleInfo方法
const getModuleInfo = (file)=>{
const body = fs.readFileSync(file,'utf-8') const ast = parser.parse(body,{ sourceType:'module' //表示咱们要解析的是ES模块 }); const deps = {} traverse(ast,{ ImportDeclaration({node}){ const dirname = path.dirname(file) const abspath = "./" + path.join(dirname,node.source.value) deps[node.source.value] = abspath } }) const {code} = babel.transformFromAst(ast,null,{ presets:["@babel/preset-env"] }) // 新增代码 const moduleInfo = {file,deps,code} return moduleInfo } 复制代码
咱们返回了一个对象 ,这个对象包括该模块的路径(file),该模块的依赖(deps),该模块转化成es5的代码
该方法只能获取一个模块的的信息,可是咱们要怎么获取一个模块里面的依赖模块的信息呢?
没错,看标题,,你应该想到了就算递归。
如今咱们来写一个递归方法,递归获取依赖
const parseModules = (file) =>{
const entry = getModuleInfo(file) const temp = [entry] for (let i = 0;i<temp.length;i++){ const deps = temp[i].deps if (deps){ for (const key in deps){ if (deps.hasOwnProperty(key)){ temp.push(getModuleInfo(deps[key])) } } } } console.log(temp) } 复制代码
讲解下parseModules方法:
目前bundle.js文件:
const fs = require('fs')
const path = require('path') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const babel = require('@babel/core') const getModuleInfo = (file)=>{ const body = fs.readFileSync(file,'utf-8') const ast = parser.parse(body,{ sourceType:'module' //表示咱们要解析的是ES模块 }); const deps = {} traverse(ast,{ ImportDeclaration({node}){ const dirname = path.dirname(file) const abspath = "./" + path.join(dirname,node.source.value) deps[node.source.value] = abspath } }) const {code} = babel.transformFromAst(ast,null,{ presets:["@babel/preset-env"] }) const moduleInfo = {file,deps,code} return moduleInfo } // 新增代码 const parseModules = (file) =>{ const entry = getModuleInfo(file) const temp = [entry] for (let i = 0;i<temp.length;i++){ const deps = temp[i].deps if (deps){ for (const key in deps){ if (deps.hasOwnProperty(key)){ temp.push(getModuleInfo(deps[key])) } } } } console.log(temp) } parseModules("./src/index.js") 复制代码
按照目前咱们的项目来讲执行完,应当是temp 应当是存放了index.js,add.js,minus.js三个模块。 ,执行看看。
牛逼!!!确实如此。
不过如今的temp数组里的对象格式不利于后面的操做,咱们但愿是以文件的路径为key,{code,deps}为值的形式存储。所以,咱们建立一个新的对象depsGraph。
const parseModules = (file) =>{
const entry = getModuleInfo(file) const temp = [entry] const depsGraph = {} //新增代码 for (let i = 0;i<temp.length;i++){ const deps = temp[i].deps if (deps){ for (const key in deps){ if (deps.hasOwnProperty(key)){ temp.push(getModuleInfo(deps[key])) } } } } // 新增代码 temp.forEach(moduleInfo=>{ depsGraph[moduleInfo.file] = { deps:moduleInfo.deps, code:moduleInfo.code } }) console.log(depsGraph) return depsGraph } 复制代码
ok,如今存储的就是这种格式啦
咱们如今的目的就是要生成一个bundle.js文件,也就是打包后的一个文件。其实思路很简单,就是把index.js的内容和它的依赖模块整合起来。而后把代码写到一个新建的js文件。
咱们把这段代码格式化一下
// index.js
"use strict" var _add = _interopRequireDefault(require("./add.js")); var _minus = require("./minus.js"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } var sum = (0, _add["default"])(1, 2); var division = (0, _minus.minus)(2, 1); console.log(sum); console.log(division); 复制代码
// add.js
"use strict"; Object.defineProperty(exports, "__esModule", { value: true}); exports["default"] = void 0; var _default = function _default(a, b) { return a + b;}; exports["default"] = _default; 复制代码
可是咱们如今是不能执行index.js这段代码的,由于浏览器不会识别执行require和exports。
不能识别是为何?不就是由于没有定义这require函数,和exports对象。那咱们能够本身定义。
咱们建立一个函数
const bundle = (file) =>{
const depsGraph = JSON.stringify(parseModules(file)) } 复制代码
咱们将上一步得到的depsGraph保存起来。
如今返回一个整合完整的字符串代码。
怎么返回呢?更改下bundle函数
const bundle = (file) =>{
const depsGraph = JSON.stringify(parseModules(file)) return `(function (graph) { function require(file) { (function (code) { eval(code) })(graph[file].code) } require(file) })(depsGraph)` } 复制代码
咱们看下返回的这段代码
(function (graph) {
function require(file) { (function (code) { eval(code) })(graph[file].code) } require(file) })(depsGraph) 复制代码
其实就是
咱们再来看下code的值
// index.js
"use strict" var _add = _interopRequireDefault(require("./add.js")); var _minus = require("./minus.js"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } var sum = (0, _add["default"])(1, 2); var division = (0, _minus.minus)(2, 1); console.log(sum); console.log(division); 复制代码
没错执行这段代码的时候,又会用到require函数。此时require的参数为add.js的路径,哎,不是绝对路径,须要转化成绝对路径。所以写一个函数absRequire来转化。怎么实现呢?咱们来看下代码
(function (graph) {
function require(file) { function absRequire(relPath) { return require(graph[file].deps[relPath]) } (function (require,code) { eval(code) })(absRequire,graph[file].code) } require(file) })(depsGraph) 复制代码
其实是实现了一层拦截。
(function (require,code) {
eval(code) })(absRequire,graph[file].code) 复制代码
return require(graph[file].deps[relPath])
这段代码,也就是执行了外面这个require
在这里
return require(graph[file].deps[relPath])
,咱们已经对路径转化成绝对路径了。所以执行外面的require的时候就是传入绝对路径。
是否是有点绕?实际上是个递归。
这样就将代码整合起来了,可是有个问题,就是在执行add.js的code时候,会遇到exports这个还没定义的问题。以下所示
// add.js
"use strict"; Object.defineProperty(exports, "__esModule", { value: true}); exports["default"] = void 0; var _default = function _default(a, b) { return a + b;}; exports["default"] = _default; 复制代码
咱们发现 这里它把exports看成一个对象来使用了,可是这个对象还没定义,所以咱们能够本身定义一个exports对象。
(function (graph) {
function require(file) { function absRequire(relPath) { return require(graph[file].deps[relPath]) } var exports = {} (function (require,exports,code) { eval(code) })(absRequire,exports,graph[file].code) return exports } require(file) })(depsGraph) 复制代码
咱们增添了一个空对象 exports,执行add.js代码的时候,会往这个空对象上增长一些属性,
// add.js
"use strict"; Object.defineProperty(exports, "__esModule", { value: true}); exports["default"] = void 0; var _default = function _default(a, b) { return a + b;}; exports["default"] = _default; 复制代码
好比,执行完这段代码后
exports = {
__esModule:{ value: true}, default:function _default(a, b) { return a + b;} } 复制代码
而后咱们把exports对象return出去。
var _add = _interopRequireDefault(require("./add.js"));
复制代码
可见,return出去的值,被_interopRequireDefault接收,_interopRequireDefault再返回default这个属性给_add,所以_add = function _default(a, b) { return a + b;}
如今明白了,为何ES6模块 引入的是一个对象引用了吧,由于exports就是一个对象。
至此,处理;两个关键词的功能就完整了。
const fs = require('fs')
const path = require('path') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const babel = require('@babel/core') const getModuleInfo = (file)=>{ const body = fs.readFileSync(file,'utf-8') const ast = parser.parse(body,{ sourceType:'module' //表示咱们要解析的是ES模块 }); const deps = {} traverse(ast,{ ImportDeclaration({node}){ const dirname = path.dirname(file) const abspath = "./" + path.join(dirname,node.source.value) deps[node.source.value] = abspath } }) const {code} = babel.transformFromAst(ast,null,{ presets:["@babel/preset-env"] }) const moduleInfo = {file,deps,code} return moduleInfo } const parseModules = (file) =>{ const entry = getModuleInfo(file) const temp = [entry] const depsGraph = {} for (let i = 0;i<temp.length;i++){ const deps = temp[i].deps if (deps){ for (const key in deps){ if (deps.hasOwnProperty(key)){ temp.push(getModuleInfo(deps[key])) } } } } temp.forEach(moduleInfo=>{ depsGraph[moduleInfo.file] = { deps:moduleInfo.deps, code:moduleInfo.code } }) return depsGraph } // 新增代码 const bundle = (file) =>{ const depsGraph = JSON.stringify(parseModules(file)) return `(function (graph) { function require(file) { function absRequire(relPath) { return require(graph[file].deps[relPath]) } var exports = {} (function (require,exports,code) { eval(code) })(absRequire,exports,graph[file].code) return exports } require('${file}') })(${depsGraph})` } const content = bundle('./src/index.js') console.log(content); 复制代码
来执行下,看看效果
确实执行成功。接下来,把返回的这段代码写入新建立的文件中
//写入到咱们的dist目录下
fs.mkdirSync('./dist'); fs.writeFileSync('./dist/bundle.js',content) 复制代码
至次,咱们的手写webpack核心原理就到此结束了。
咱们参观下生成的bundle.js文件
发现其实就是将咱们早期收集的全部依赖做为参数传入到当即执行函数当中,而后经过eval来递归地执行每一个依赖的code。
如今咱们将bundle.js文件引入index.html看看能不能执行
成功。。。。。惊喜。。
感谢您也恭喜您看到这里,我能够卑微的求个star吗!!!
github:https://github.com/Sunny-lucking/howToBuildMyWebpack
本文使用 mdnice 排版