美文网首页让前端飞
webpack核心功能实现思路

webpack核心功能实现思路

作者: size_of | 来源:发表于2020-02-02 22:39 被阅读0次

    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核心功能实现思路,欢迎交流

    相关文章

      网友评论

        本文标题:webpack核心功能实现思路

        本文链接:https://www.haomeiwen.com/subject/dhwtxhtx.html