Skip to content

从0实现一个webpack #1

Open
@coderzzp

Description

@coderzzp

从0实现一个webpack

什么是webpack?

WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(jsx,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。如果你不了解webpack的用法,可以转到阮一峰的webpack教程先学习一下webpack的用法。

webpack最简单的用法

git clone https://github.com/coderzzp/how2-learn-nodejs.git
cd how2-learn-nodejs
cd webpack-demo

这是一个最简单的webpac的例子,可以简单看一下文件目录,之后继续执行

npm i 
npm run dev

命令行出现
image

即打包完成。
看到目录下多了一个�dist文件,里面有一个bundle文件,这其实就是在执行npm run dev即package.json脚本中的webpack命令,此时我们打开页面

open index.html

image

页面输出hello world~,显然页面里的./dist/bundle.js就是以index.js为入口文件打包后的文件

bundle.js是怎么打包index.js+words.js的呢?

我们打开dist/bundle.js来看一下,虽然只有100行的代码一眼看过去因为各种注释的关系显得很乱,但是仔细看一下整个代码结构实际上是一个自执行函数,直接传入一个对象即mudules.

(function(modules) {
})({
   "./index.js": function(...){...},
   "./words.js": function(...){...}
})

而这里的modules正是我们需要打包的文件对象,类似

{
  "文件位置":函数,
  "文件位置":函数
}

那么我们再来看看自执行函数内部是如何使用mudules这个对象的,首先看这个函数的返回值是:

return __webpack_require__(__webpack_require__.s = "./index.js");

接着看_webpack_require__这个最核心的函数:

/******/ 	//缓存对象
/******/    var installedModules = {};
/******/ 	// 定义require函数
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// 如果缓存对象中有,即取出返回值
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// 创建一个mudule对象
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// 执行mudules对象中的函数,传入(module,module.exports,__webpack_require__)
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}

这段代码中最重要的代码如下,利用call方法执行modules中的代码

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

传入了三个参数:module, module.exports,和__webpack_require_,再回过头来看mudules对象用这些参数做了什么?
index.js:
image
主要是将require替换成传入的__webpack_require_,相当于在index.js中我们每次requrie时,会调用__webpack_require_这个函数,递归策略~

words.js:
image
想象一下这个bundle.js执行的过程,函数调用栈先执行index.js中的代码,此时在代码中会碰到__webpack_require('./words.js),那么调用栈继续执行words.js中的代码,words.js通过module.exports导出’hello world‘,退出words.js调用栈,回到index.js,继续执行剩余代码,然后返回module.exports,因为index.js是入口文件,module.exports={},代码执行结束。

理解AST的存在

好了,交代完原理,无论你是了然于胸还是一知半解,都一起来跟着做一个webpack吧,毕竟实践出真知。
但是这一节明显还没有动手的意思,先了解一下什么是AST抽象语法树,AST抽象语法树是编译器经常会涉及到的一个概念,代码=>AST=>转换=>生成=>机器理解的代码,AST通常是树形结构,在js中用对象来表示这个树形结构。可以在这个网站尝试将代码转换为AST,大致理解了AST,可是这里为什么要用到AST呢?

  • 将ES6代码转换为ES5(通过babel生成AST并将AST转换为普通的ES5代码�)
  • 生成代码依赖,即生成modules对象(babel依旧可以完成这个任务)
    image

完成代码

直接上代码吧:定义了三个辅助函数getAst,getDependence,ast2code

//获取文件,解析成ast语法
function getAst (filename) {
  const content = fs.readFileSync(filename, 'utf-8')
  return babylon.parse(content, {
    sourceType: 'module',
  });
}

 //通过ast得到该代码的依赖 
function getDependence (ast) {
  let dependencies = []
  traverse(ast, {
    ImportDeclaration: ({node}) => {
      dependencies.push(node.source.value);
    },
  })
  return dependencies
}

//通过ast生成code
function ast2code(ast) {
  const {code} = transformFromAst(ast, null, {
    presets: ['env']
  });
  return code
}

基于上面的三个函数,生成了一个解析代码的函数

//解析代码,生成完整的文件依赖关系映射

function parse(fileName, entry) {
  let filePath = fileName.indexOf('.js') === -1 ? fileName + '.js' : fileName
  let dirName = entry ? '' : path.dirname(config.entry)
  let absolutePath = path.join(dirName, filePath)
  const ast = getAst(absolutePath)
  return {
    fileName,
    dependence: getDependence(ast),
    code: ast2code(ast),
  };
}

这个时候我们是完成了ES6到ES5的转换,并且我们得到了入口文件的所有dependence(依赖),之前我们提到,所有依赖也要解析,于是有了下面的代码,递归解析所有的依赖

//获取深度队列依赖关系,类似于递归调用,解析并获得所有解析后的模块

function getQueue(main) {
  let queue = [main]
  for (let asset of queue) {
    asset.dependence.forEach(function (dep) {
      let child = parse(dep)
      queue.push(child)
    })
  }
  return queue
}

我们得到了这个queue相当于是我们项目所需要的所有文件被解析之后的代码,现在只需要执行最后一步:打包:

function bundle(queue) {
  let modules = ''
  //先通过queue生成modules映射对象
  queue.forEach(function (mod) {
    modules += `'${mod.fileName}': function (require, module, exports) { ${mod.code} },`
  })
  //
  const result = `
    (function(modules) {
      function require(fileName) {
        const fn = modules[fileName];
        const module = { exports : {} };
        fn(require, module, module.exports);
        return module.exports;
      }
      require('${config.entry}');
    })({${modules}})
  `;

  // We simply return the result, hurray! :)
  return result;
}

是不是跟我们之前解析的webpack的代码如出一辙,到这一步就基本完成了一个简易的webpack了
完整代码地址戳这里

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions