webpack运行原理

120 阅读3分钟

1.通过入口文件查找依赖

2.遍历递归依赖

3.转换成代码

4.拼接成立即执行函数,输出

整体编译转换功能主要由Compiler对象实现,

Compiler主要结构如下

  • run():开始编译

  • buildModule():构建模块

  • emitFiles():输出文件

const {getAST,getDependencies,transform} = require("./parser") //引入工具函数
class Compiler{
   //接收实例化时传入的webpack配置文件参赛
    constructor(options){
        const {entry,output} = options;
        this.entry = entry; //入口
        this.output = output; //出口
        this.modules = []
    }
    //开启编译
    run(){
       //递归调用buildModule,将模块及其依赖都存入modules数组中
       const entryModule = this.buildModule(this.entry,true);
        this.modules.push(entryModule)
        this.modules.map((_module)=>{
            _module.dependencies.map((dependency)=>{
                this.modules.push(this.buildModule(dependency));
            })
        })
       //然后调用输出文件
        this.emitFiles();
    }
    //构建模块相关
    buildModule(filename,isEntry){
      //将文件都转化成filename,dependencies,transformCode类型的对象
        let ast;
        if(isEntry){//如果是入口文件路径,就不用拼接路径,直接传入
            ast = getAST(filename)
        }else{
            const absolutePath = path.join(process.cwd(),"./src",filename);
            ast = getAST(absolutePath)
        }
        return {
            filename,//文件名称
            dependencies:getDependencies(ast),//依赖列表
            transformCode:transform(ast),//转化后的代码
        }
    }
    //输出文件
    emitFiles(){
        const outputPath = path.join(this.output.path,this.output.filename);
        let modules = "";
      //构建成文件路径,函数的形式
        this.modules.map((_module)=>{
            modules +=`'${_module.filename}':function(require,module,exports) {${_module.transformCode}},`
        })

        const bundle = `
        (function(modules){
            function require(fileName){
                const fn = modules[fileName];
                const module = {exports:{}};
                fn(require,module,module.exports)
                return module.exports
            }
            require('${this.entry}')
        })({${modules}})`;
        fs.writeFileSync(outputPath,bundle,"utf-8")
    }
}

输出文件函数emitFiles这里看着一大坨,有点复杂,我们单独拿出来

这里先看一下webpack打包后的精简代码:

// dist/index.xxxx.js
(function(modules) {
  // 已经加载过的模块
  var installedModules = {};

  // 模块加载函数
  function __webpack_require__(moduleId) {
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    module.l = true;
    return module.exports;
  }
  __webpack_require__(0); //启动函数
})([
/* 0 module */
(function(module, exports, __webpack_require__) {
  ...
}),
/* 1 module */
(function(module, exports, __webpack_require__) {
  ...
}),
/* n module */
(function(module, exports, __webpack_require__) {
  ...
})]);

webpack会把一个个模块,都构建成函数

function(module, exports, webpack_require){模块编译后的代码}

这样的形式,然后存入一个数组,然后整体就是一个立即执行函数,将这个数组作为参数。

函数内部定义了一个对象installedModules,用数组下标作为key,如果对象中已经存在该模块,则返回该模块内容,否则调用该函数,返回内容

emitFile函数也是实现了和上面类似的功能:

emitFiles(){
        const outputPath = path.join(this.output.path,this.output.filename);
        let modules = "";
      //构建成{文件路径:函数}的形式
        this.modules.map((_module)=>{
            modules +=`'${_module.filename}':function(require,module,exports) {${_module.transformCode}},`
        })

        const bundle = `
        (function(modules){
            function require(fileName){
                const fn = modules[fileName];
                const module = {exports:{}};
                fn(require,module,module.exports)
                return module.exports
            }
            require('${this.entry}') //类似于上面的 __webpack_require__(0),启动函数
        })({${modules}})`;
        fs.writeFileSync(outputPath,bundle,"utf-8")
    }

Compiler中导入了3个工具函数,还需要我们实现一下

  • getAST:将模块转换成ast树(用@babel/parser的parse去转换)
  • getDependencies:收集模块中的依赖(用@babel/traverse的traverse去转换)
  • transform:将模块中的es6代码转换成es5(用babel-core的transformFromAst去转换)

具体实现如下:

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const {transformFromAst} = require("babel-core");
getAST:(path) =>{
        const source = fs.readFileSync(path,"utf-8")
        return parser.parse(source,{
            sourceType:"module", //表示解析的是es模块
        })
    },
    //对AST节点进行递归遍历
    getDependencies:(ast)=>{
        const dependencies = [];
        traverse(ast,{
            ImportDeclaration:({node})=>{
                dependencies.push(node.source.value);
            }
        })
        return dependencies
    },
    //将获得的es6的ast转化成es5
    transform:(ast)=>{
        const {code} = transformFromAst(ast,null,{
            presets:["env"],
        });
        return code;
    }

以上就实现了Compiler对象

然后我们将其 new 一下,导入webpack配置项文件,就像这样

const Compiler = require('./compiler')
const options = require("../forestpack.config") //webpakc配置项文件
new Compiler(options).run()
const path = require("path")

module.exports ={
    entry:path.join(__dirname,"./src/index.js"),
    output:{
        path:path.join(__dirname,"./dist"),
        filename:"bundle.js",
    }
}

然后运行命令 node lib/index.js,就能在dist文件夹看到与webpack大差不差的bundle了,然后导入index.html运行,就能看到最终效果。

文章参考:github.com/Cosen95/blo…

项目代码:github.com/webfamer/my…