webpack核心功能实现思路

webpack是一个功能丰富且复杂的打包工具,使用时需要掌握Loader、Plugin等等概念,不过其核心功能就是将浏览器看不懂的代码翻译成可执行代码,为了快速掌握webpack的实现思路,让我们抛开那些繁琐的概念,看看打包工具是如何翻译模块化代码的。

webpack核心功能切入点

现有的commonJS规范和es6模块化方案等浏览器并不支持,也就是说我们在node环境下执行的好好的require、exports浏览器无法识别。现在以commonJS规范为例,如果我们使用commonJS进行模块化,首先要解决的问题是如何让浏览器识别requireexports

先观察一下require这个关键字,我们会发现它实际上就是一个函数,接收的参数是一个路径,只不过在node环境下天然存在这样一个函数供你使用。浏览器不认识require是因为浏览器并没有帮你去声明require。所以打包工具要做的就是要实现一个require取代代码中的require。那么问题又来了,怎么才能在不改动源码的情况下代替原有require呢?其实答案很明显了,把我们实现的require当做参数传递给这个模块就好了。

实际上现在我们的模块化、以单文件形式进行作用域隔离等,在之前都是使用立即执行函数去做的,我们可以借鉴前辈们的方法,将模块中的内容放入一个函数中,将自定义的requireexports作为实参传递给这个函数,从而达到替换原有requireexports的作用。

let what = require('./eat')
let where = require('./run')
exports.name = `mayun eat ${whatObj.what} run to ${whereObj.where}`

//转换为下面的形式
function(require,exports){
  let what = require('./eat')
  let where = require('./run')
  exports.name = `mayun eat ${whatObj.what} run to ${whereObj.where}`
}

modules结构雏形

现在我们需要将这些松散的模块组织在一起,将他们放入对象中是一种不错的形式,我们给这个对象起个名字modules。同时,每个模块都需要一个名字方便我们找到它,所以我们给每个模块一个不重复的id

let modules = {
  0: function (require, exports) {
    let whatObj = require('./eat')
    let whereObj = require('./run')
    exports.action = `mayun eat ${whatObj.what} run to ${whereObj.where}`
  },
  1: function (require, exports) {
    exports.what = '火锅'
  },
  2: function (require, exports) {
    exports.where = '北京'
  },
}

模块代码执行函数exec

接下来就需要去声明一个require方法和一个函数,让这个函数去执行modules中的函数,我们给它起名叫exec。大概像这样:

function exec(id) {
  let fn = modules[id]
  let exports = {}
  // 模拟 require 语句
  function require(path) {

  }
  // 执行存放所有模块数组中的第0个模块
  fn(require,exports)
}
exec(0)

模块依赖映射mapping

bundle.js(对,就是webpack打包后生成的那个东西)中最核心的代码就是modulesexec函数,实际上现在我们已经得到了bundle.js的雏形。require函数的实现我们先放在一边,现在再来思考一个问题,打包工具需要通过原require中的路径找到对应的模块,但是modules对象被整合出来后,各个模块代码脱离了之前的位置,所以我们很难再通过这个相对路径去寻找对应的模块文件了。既然我们已经抽离出需要的模块代码,我们是不是可以直接做一个映射,将相对路径和被抽离出来的模块对应起来呢?为了让每个模块都可以通过这个映射找到依赖模块,我们就给这个模块加一个mapping,正好现在模块id和代码已经一一对应了,修改一下modules的结构即可。我们让模块id对应一个数组,之前的模块代码现在放在数组第0个位置,它的mapping放在数组的第1个位置

let modules = {
  0: [function (require, exports) {
    let whatObj = require('./eat')
    let whereObj = require('./run')
    exports.action = `mayun eat ${whatObj.what} run to ${whereObj.where}`
  }, {
    './eat': 1,
    './run': 2,
  }
  ],
  1: [function (require, exports) {
    exports.what = '火锅'
  }, {}
  ],
  2: [function (require, exports) {
    exports.where = '北京'
  }, {}
  ],
}

按照新的数据结构,我们调整一下exec函数的实现:

function exec(id) {
  let [fn,mapping] = modules[id]
  let exports = {}
  fn(require, exports)

  function require(path) {
    return exec(mapping[path])
  }
  
  return exports
}
exec(0)

当目前为止我们已经做到使用exec函数可以顺利执行转换后的modules了,所以接下来的重点就是如何将模块文件读取出来生成modules。先捋清思路,首先我们需要读取入口文件,拿到入口文件的依赖,同时将入口文件代码和依赖组成数组追加到modules中;拿到依赖后,读取依赖文件,重复上一步操作。很显然这时候我们需要用到nodejs。不管怎么说,我们先实现一个拿到模块代码中依赖项的方法,可以使用正则去匹配require中的路径:

// 获取模块依赖数组
function getDependencies(str){
  let reg = /require\(['"](.+?)['"]\)/g
  let result = null
  let dependencies = []
  // 通过正则匹配到require括号中的相对路径,存放在数组中
  while(result = reg.exec(str)){
    dependencies.push(result[1])
  }
  return dependencies
}

此时将读取的文件内容作为参数传递给getDependencies即可:

// 获取入口文件内容
let fileContent = fs.readFileSync('./index.js','utf-8')
console.log(getDependencies(fileContent))

[ './people.js' ]

读取modules中的模块项

这时候我们回过头看一下modules的结构,既然需要得到关于这个模块的多种信息,我们最好是封装一个函数返回这个模块的信息:

// 全局变量 作为模块的id
let id = 0
// 根据文件路径获取文件信息并生成一个对象
function getModule(filename){
  let fileContent = fs.readFileSync(filename,'utf-8')
  return {
    id:id++,
    filename:filename,
    dependencies:getDependencies(fileContent),
    code:`function(require,exports){
         ${fileContent} 
    }`,
  }
}

生成资源列表Graph

现在我们有了入口文件对象的信息,可以将它放在一个数组里,接下来就是根据这个入口对象的依赖获取到依赖模块对象信息,并且push到对象数组中,生成一个资源列表。现在我们来实现这个函数:

// 传入入口文件路径,生成模块数组(资源列表)
function getGraph(filename){
  let indexModule = getModule(filename)
  let graph = [indexModule]

  // tips:这里使用for of非常便利,因为循环后数组项会动态增加,
  // for of语句会在已经循环过的基础上继续循环,而不会从头再循环一次
  for(let value of graph){
    value.mapping = {}
    value.dependencies.forEach((relativePath)=>{
      const absolutePath = path.join(__dirname,relativePath)
      let module = getModule(absolutePath)
      value.mapping[relativePath] = module.id
      graph.push(module)
    })
  }
  return graph
}
// graph
[ { id: 0,
    filename: './index.js',
    dependencies: [ './people.js' ],
    code:
     'function(require,exports){\n         let todo = require(\'./people.js\')\nconsole.log(todo)\n \n    }',
    mapping: { './people.js': 1 } },
  { id: 1,
    filename: '/Users/fengjixuan/Downloads/webpack-simple/people.js',
    dependencies: [ './eat.js', './run.js' ],
    code:
     'function(require,exports){\n         let whatObj = require(\'./eat.js\')\nlet whereObj = require(\'./run.js\')\nexports.action = `mayun eat ${whatObj.what} run to ${whereObj.where}`\n\n\n \n    }',
    mapping: { './eat.js': 2, './run.js': 3 } },
  { id: 2,
    filename: '/Users/fengjixuan/Downloads/webpack-simple/eat.js',
    dependencies: [],
    code:
     'function(require,exports){\n         exports.what = \'火锅\' \n    }',
    mapping: {} },
  { id: 3,
    filename: '/Users/fengjixuan/Downloads/webpack-simple/run.js',
    dependencies: [],
    code:
     'function(require,exports){\n         exports.where = \'北京\' \n    }',
    mapping: {} } ]

生成bundle.js

准备工作都已经做好,现在只需要把上面获取到的数据转换成modules,再把modulesexec函数拼接成字符串写入到一个名为bundle.js的文件中,这个js文件就可以无障碍的在浏览器中执行了:

// 生成浏览器可执行的代码并写入bundle.js中
function createBundle(graph) {
  let modules = ''
  // 生成modules字符串
  graph.forEach((module) => {
    modules += `${module.id}:[
      ${module.code},
      ${JSON.stringify(module.mapping)}
    ],`
  })
  // 生成立即执行函数,并且将moudules作为参数传递进去
  let result = `(function f(modules) {
    function exec(id) {
      let [fn, mapping] = modules[id]
      let exports = {}
      fn && fn(require, exports)

      function require(path) {
        return exec(mapping[path])
      }

      return exports
    }

    exec(0)
  })({${modules}})`

  // 写入到bundle.js中
  fs.writeFileSync('./dist/bundle.js',result)
}

以上就是webpack核心功能实现思路,欢迎交流

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容