webpack手写打包

395 阅读4分钟

欢迎来到MyDarlingBug的高级私人会所。

9437320abdd9b85dedca127498e3537.png

提问

你是否面临着这样一个困扰?

  • 面试官:webpack打包原理是什么?
  • 我: 打包原理是不啦不啦(内心窃喜,呵,这里我看过。老套路~八股文把你安排了!)
  • 面试官:那正好这里有电脑,能不能敲一个出来,简单的demo也行。
  • 我:黑人❓❓❓。 额,这个这个,我试试~~
  • 面试官:30 minutes later。。。 你可能这方面还要加强一点,有机会再合作。

未标题-1.png

😔,为了恰饭,还是自己总结总结。

1.准备工作

在同级目录新建四个文件,分别为index.js(入口文件) info.js,arr.js, kwebpack.js。

5f25fe51d4e8c2fe7aea980b33ab6d3.png

//index.js 文件内容 
import info from './info.js';
import arr from './arr.js';
let a = 2;
console.log(a);
console.log(info,'-----',arr)

//info.js 文件内容
console.log('info')
let info = 123
export default info

//arr.js 文件内容
console.log('arr')
let arr = 123
export default arr

2.编写打包方法

  1. 先安装fs,parser模块。fs用来解析文件内容,注意用utf-8格式。 parser.parse方法用来生成ast抽象语法树(就是个对象,方便处理整个代码文件)。(具体如何生成,想深入理解(不嫌麻烦)也可以自己从词法解析 语法解析开始构建ast )
//kwebpack.js 文件内容
const fs = require('fs')
const parser = require('@babel/parser')

function kwebpack(entry){
  const file = fs.readFileSync(entry,'utf-8');
  const ast = parser.parse(file,{sourceType:'module'})   // 默认不支持esModule,所以要声明一下
}

kwebpack('./index.js')
  1. 我们把index.js 当作项目入口,同时需要在kwebpack方法收集入口文件里其他的模块,方便后续遍历查询整个项目。
  const traverse = require('@babel/traverse').default //使用当前api解析
  
  //kwebpack方法
  function kwebpack(entry){
  ...
  const dependencies = []
    traverse(ast, {
        ImportDeclaration(res) {   // 解析import
            console.log(res, '--res--')
            const value = res.node.source.value
            dependencies.push(value)
            //dependencies = ['./info.js','./arr.js']
        }
  }
  ...
  }

返回的res结构,可以看到有一个value 可以获取到./info.js, ./arr.js

ef2001e2a8f0ff4a48b04432e3ac9cb.png

3.使用babel插件 把es6转换成es5.

  const traverse = require('@babel/traverse').default //使用当前api解析
  const { transformFromAst } = require('@babel/core')
  //kwebpack方法
  function kwebpack(entry){
  ...
  const dependencies = []
    traverse(ast, {
      ...
   }
   
   const { code } = transformFromAst(ast, null, {
        presets: ['@babel/preset-env']   //这是插件套餐
    })

    return {
        code,
        dependencies,
        entry
    }
  ...
  }

可以看到kwebpack方法输出的内容

const res = kwebpack('./index.js')
//res
{
  code: '"use strict";\n' +
    '\n' +
    'var _info = _interopRequireDefault(require("./info.js"));\n' +
    '\n' +
    'var _arr = _interopRequireDefault(require("./arr.js"));\n' +
    '\n' +
    'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
    '\n' +
    'var a = 2;\n' +
    'console.log(a);\n' +
    `console.log(_info["default"], '-----', _arr["default"]);`,
  dependencies: [ './info.js', './arr.js' ],
  fileName: './index.js'
} 
  1. 接下来我们用广度优先遍历查找到所有模块,让每一个模块都通过Kwebpack方法输出{code,dependencies,fileName}对象。
...
function rangeLoop(entry) {

    const oneObjs = kwebpack(entry)
    const graph = {}   //收集所有 {code,dependencies,fileName}
    const allArr = [oneObjs]
    
    graph[entry] = oneObjs
    
    while (allArr.length) {
    
        const oneObj = allArr[0];
        
        const dependencies = oneObj.dependencies;
        
        for (let dep of dependencies) {
        
            const item = kwebpack(dep)  
            allArr.push(item)
            graph[dep] = item
            
        }
        allArr.shift()
    }
    
    return graph
}
...

var graph = rangeLoop('./index.js')

可以看到graph输出为:

{
  './index.js': {
    code: '"use strict";\n' +
      '\n' +
      'var _info = _interopRequireDefault(require("./info.js"));\n' +
      '\n' +
      'var _arr = _interopRequireDefault(require("./arr.js"));\n' +
      '\n' +
      'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
      '\n' +
      'var a = 2;\n' +
      'console.log(a);\n' +
      `console.log(_info["default"], '-----', _arr["default"]);`,
    dependencies: [ './info.js', './arr.js' ],
    fileName: './index.js'
  },
  './info.js': {
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports["default"] = void 0;\n' +
      "console.log('info');\n" +
      'var info = 123;\n' +
      'var _default = info;\n' +
      'exports["default"] = _default;',
    dependencies: [],
    fileName: './info.js'
  },
  './arr.js': {
    code: '....',
    dependencies: [],
    fileName: './arr.js'
  }
}
  1. 拿到模块对象之后,我们需要拼写字符串让浏览器可以递归运行对象里的每一段code。 注意:
  • code里可能有require方法,我们需要编写require。
  • code里可能有exports,需要把exports对象返回。
  • 浏览器不会帮忙补齐 " ; " 号,平时是js帮忙补齐,注意要手动添加;号
function getJsString(entry) {
    const graph = rangeLoop(entry);    //这里获取到graph模块对象  
    return `(function(graph){   
      function require(module){        
        function localRequire(relative){  //编写require方法。进行递归
          return require(relative)
        };

        var exports = {};               

        (function(require,exports,code){
            eval(code)                  
        })(localRequire,exports,graph[module].code);
        
        return exports
      };

      require('${entry}');

    })(${JSON.stringify(graph)})`
}

const res = getJsString('./index.js');

总之,这段代码就做了三件事。

  • 用eval 方法执行当前对象code。
  • 新建一个exports对象,把当前对象code里的exports return返回。
  • 找到当前对象code里的require方法,继续递归往下执行。
  • 最后把模板返回。
  1. 把getJsString方法获取到的res对象粘贴到浏览器

e707d138cc182298c1f15ad51efc92c.png

可以看到结果已经输出,说明打包成功。

注意:

可能会遇到如下情况,浏览器禁止使用eval。也可以修改配置或者更换浏览器尝试。

2cc8f2b3472d5d201ebb789829e90c8.png

完整代码:

//kwebpack.js

const fs = require('fs')
const parser = require('@babel/parser')
const { transformFromAst } = require('@babel/core')
const traverse = require('@babel/traverse').default
const path = require('path')

function kwebpack(fileName) {
    const file = fs.readFileSync(fileName, 'utf-8');
    const ast = parser.parse(file, { sourceType: 'module' })
    const dependencies = []
    traverse(ast, {
        ImportDeclaration(res) {
            const value = res.node.source.value
            const dirname = path.dirname(fileName)
            const newValue = "./" + path.join(dirname, value)
            dependencies.push(newValue)
        }
    })

    const { code } = transformFromAst(ast, null, {
        presets: ['@babel/preset-env']
    })

    return {
        code,
        dependencies,
        fileName
    }
}

function rangeLoop(entry) {

    const oneObjs = kwebpack(entry)
    const graph = {}
    const allArr = [oneObjs]
    graph[entry] = oneObjs
    while (allArr.length) {
        const oneObj = allArr[0];
        const dependencies = oneObj.dependencies;
        for (let dep of dependencies) {
            const item = kwebpack(dep)
            allArr.push(item)
            graph[dep] = item
        }
        allArr.shift()
    }
    return graph
}


function getJsString(entry) {
    const graph = rangeLoop(entry);
    console.log(graph,'---graph-')
    return `(function(graph){
      function require(module){
        function localRequire(relative){
          return require(relative)
        };

        var exports = {};

        (function(require,exports,code){
            eval(code)
        })(localRequire,exports,graph[module].code);
        
        return exports
      };

      require('${entry}');

    })(${JSON.stringify(graph)})`
}

const res = getJsString('./index.js');


知识点提炼:

  1. 加强webpack打包原理的理解。

  2. 广度优先遍历使用。

  3. 理解生成webpack模板字符串的大致过程。

  4. 了解打包过程中babel部分api的用法等。

最后

看官老爷请注意,文中有任何叙述错误均可留言评论指出,不甚感谢。同时欢迎交流讨论。看官老爷请注意,文中有任何叙述错误均可留言评论指出,不甚感谢。 同时欢迎交流讨论。

我是MyDarlingBug,大家一起来开开心心写Bug。

未标题-1.png