手写 webpack 教程

先来看示例代码。

我们这里三个文件,index.js 是主入口文件:

// filename: index.js
import foo from './foo.js'
foo();

//filename: foo.js
import message from './message.js'
function foo() {
  console.log(message);
}

// filename: message.js
const message = 'hello world'
export default message;
复制代码

接下来,我们会创建一个 bundle.js 打包这三个文件,打包得到的结果是一个 JS 文件,运行这个 JS 文件输出的结果会是 'hello world'。

bundle.js 就是 webpack 做的事情,我们示例中的 index.js 相当于 webpack 的入口文件,会在 webpack.config.js 的 entry 里面配置。

让我们来实现 bundle.js 的功能。

最开始的,当然是读主入口文件了:

function createAssert(filename) {
  const content = fs.readFileSync(filename, {
    encoding: 'utf-8'
  });
  return content;
} 

const content = createAssert('./example/index.js');
复制代码

image.png

接下来,需要做的事情就是把 import 语法引入的这个文件也找过来,在上图中,就是 foo.js,同时还得把 foo.js 依赖的也找过来,依次递推。

现在得把 foo.js 取出来,怎么解析 import foo from './foo.js' 这句,把值取出来呢?

把这行代码解析成 ast 会变成:

image.png

接下来的思路就是把上面的代码转化成 ast,接着去取上图框框里那个字段。对依赖的文件也进行读取操作:

const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;

function createAssert(filename) {
  const dependencies = [];
  const content = fs.readFileSync(filename, {
    encoding: 'utf-8'
  });

  const ast = babylon.parse(content, {
    sourceType: 'module',
  }); 
  
  traverse(ast, {
    ImportDeclaration: ({node}) => {
      dependencies.push(node.source.value);
    }
  })

  console.log(dependencies); // [ './foo.js' ]
  return content;
}
复制代码

上面我们做的事情就是把当前的文件读到,然后再把当前文件的依赖加到一个叫做 dependencies 的数组里面去。

然后,这里的 createAssert 只返回源代码还不够,再完善一下:

let id = 0;

function getId() { return id++; }

function createAssert(filename) {
  const dependencies = [];
  const content = fs.readFileSync(filename, {
    encoding: 'utf-8'
  });

  const ast = babylon.parse(content, {
    sourceType: 'module',
  }); 

  traverse(ast, {
    ImportDeclaration: ({ node }) => {
      dependencies.push(node.source.value);
    }
  })

  return {
    id: getId(),
    code: content,
    filename,
    dependencies,
    mapping: {},
  };
}
复制代码

假如对主入口文件 index.js 调用,得到的结果会是(先忽略 mapping):

image.png

我们不能只对主入口文件做这件事,得需要对所有在主入口这链上的文件做,上面 createAssert 针对一个文件做,我们基于这个函数,建一个叫做 crateGraph 的函数,里面进行递归调用:

function createGraph(entry) {
  const modules = [];

  createGraphImpl(
    path.resolve(__dirname, entry),
  );

  function createGraphImpl(absoluteFilePath) {
    const assert = createAssert(absoluteFilePath);
    modules.push(assert);

    assert.dependencies.forEach(relativePath => {
      const absolutePath = path.resolve(
        path.dirname(assert.filename),
        relativePath
      );
      const child = createGraphImpl(absolutePath, relativePath);
      assert.mapping[relativePath] = child.id;
    });

    return assert
  }

  return modules;
}
复制代码

运行这个函数,得到的结果如下图所示:

image.png

大家可以注意到,截图中,数组中每一项的 code 就是我们的源代码,但是这里面还残留这 import 语句,我们先使用 babel 把它转成 commonJS 。

做的也比较简单,就是用 babel 修改 createAssert 中返回值的 code:

const code = transformFromAst(ast, null, {
  presets: ['env'],
}).code
复制代码

截取其中一项,结果变成了:

image.png

接下来要做的一步刚上来会比较难以理解,最关键的是我们会重写 require 函数,不妨先看:

我们新建一个函数 bundle 来处理 createGraph 函数得到的结果。

function bundle(graph) {
  let moduleStr = '';

  graph.forEach(module => {
    moduleStr += `
    ${module.id}: [
      // require,module,exports 作为参数传进来
      // 在下面我们自己定义了,这里记作位置 1
      function(require, module, exports) {
        ${module.code}
      },
      ${JSON.stringify(module.mapping)}
    ],
    `
  })

  const result = `
    (function(modules){
      function require(id) {
        const [fn, mapping] = modules[id];

        const module = { exports: {} }
        
        // fn 就是上面位置 1 那个函数
        fn(localRequire, module, module.exports)
       
        function localRequire(name) {
          return require(mapping[name])
        }       

        return module.exports;
      }

      require(0);
    })({${moduleStr}}) 
  `
  return result;
}
复制代码

最终的使用就是:

const graph = createGraph('./example/index.js');
const res = bundle(graph);
复制代码

res 就是最终打包的结果,复制整段到控制台运行,可见输出:

image.png

于是基本的功能就成功了。

(今天有点晚了,未完待续,明天我继续添加依赖成环问题、包缓存问题的解决)

猜你喜欢

转载自juejin.im/post/7050500872920367118