深入了解Webpack---Bundler 源码编写

通过自己编写打包代码,可以更好的了解打包的流程以及webpack打包原理。

案例:实现一个类似于webpack的打包工具,也是基于nodejs。

目录结构:

//word.js:
export const word = "hello"; 

//message.js:
import { word } from "./word.js";  //要去实现类似于webpack的打包工具,即还没有webpack的功能,因此必须写全引用
export const message = `say ${word}`;

//index.js:
import { message } from "./message.js";  //一定要加后缀名,目前模块语法不支持
console.log(message);

 流程说明:

  • 单个模块分析
  • 生成整个项目的Dependencies Graph
  • 生成代码

一、模块分析

1.要实现对项目打包,首先需要引入入口文件,之后读取到入口文件中的内容并分析

const fs = require("fs");

const moudleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, "utf-8"); //1-2、获取到入口文件中的内容
  console.log(content);
};

moudleAnalyser("./src/index.js"); //1-1、引入入口文件

编译结果如下: 

安装高亮显示代码的工具:

npm install cli-highlight -g 
node bundler.js | highlight //若是输出stdout is not a tty代表权限不足,用cmd即可看到效果

高亮后效果如下:

2、分析依赖模块:获取到入口文件的源码后,分析源码,找出依赖模块

借助于@babel/parser获取文件中的依赖关系:https://babeljs.io/docs/en/babel-parser

//npm install @babel/parser --save

//根据源码获取抽象语法树AST
const ast = parser.parse(content, {
    sourceType: "module", //ES Module方式引入模块
});

console.log(ast)

 如下输出:期中program就是我们的代码,看body,打印出内容:console.log(ast.program.body)

       

以上输出两个节点:

  • 第一个:type说明时引用声明,value为引入的模块
  • 第二个:type说明是表达式

@babel/traverse:由上可知:@babel/parser可以找出源码中的依赖关系,但是需遍历整个分析结果找出type是引用声明的,才能找出该文件中所有的依赖模块=》自己写遍历太麻烦,借用@babel/traverse找出源码中所有的引用关系:

//npm install @babel/traverse --save

const traverse = require("@babel/traverse").default; //导出使用的是import from-ES Module形式,必须加默认,默认就是ES M形式
 traverse(ast, {
    ImportDeclaration({ node }) {
      console.log(node);
    },
  });

 输出如下,和ast.program.body相比只输出了ImportDeclaration的node,上面是输出两个node:

看上面输出,可知node.source.value就是找出的依赖模块

 const dependencies = []

  traverse(ast, {
    ImportDeclaration({ node }) {//找出抽象语法树中type为ImportDeclaration
      dependencies.push(node.source.value)
    }
  });

console.log(dependencies);

输出如下: 

问题1:数组中模块的路径是相对路径,而且是相对于入口文件index.js的,有上面的路径可知,入口文件和bundler.js不在同一文件夹内,所以在bundler.js处理的时候需绝对路径或相对于根目录的路径

//const path = require("path");
console.log(path.dirname(filename))

查看路径如下:

修改路径如下: 

const dirname = path.dirname(filename);
const newFile = "./" + path.join(dirname, node.source.value);
console.log(newFile);
dependencies.push(newFile);

 

为了以后方便将绝对路径和相对路径都保存下来:

const dependencies = {};
dependencies[node.source.value] = newFile;

 

问题2:以上打包的是ES6语法,还需要传换成所有浏览器都支持的语法

//npm install @babel/core --save
//npm install @babel/preset-env --save

const babel = require("@babel/core");
const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
});
console.log(code);

如下输出,import from转换后为require: 

       

到此,整个入口文件分析完毕,返回分析结果:

const moudleAnalyser = filename=>{
    //.....
    return {
        filename,
        dependencies,
        code,
  };
}  

const result = moudleAnalyser("./src/index.js"); //1-1、引入入口文件
console.log(result);

输出如下:

二、生成整个项目的依赖图谱

上面分析了单个模块,接下来就需要对入口文件依赖的模块进行如上分析,之后就是依赖的依赖的分析,把所有涉及的模块都需分析,即递归所有模块,分析后产生项目依赖图谱,才可以打包。

const makeDependenciesGraph = (entry) => {
  const entryMoudle = moduleAnalyser(entry); //1-2、入口文件分析

  //通过队列方式实现类似递归效果
  const graphArr = [entryMoudle];
  for (let i = 0; i < graphArr.length; i++) {
    const item = graphArr[i];
    const { dependencies } = item;
    if (dependencies) {
      for (let dependece in dependencies) {
        graphArr.push(moduleAnalyser(dependencies[dependece]));
      }
    }
  }
  //console.log(graphArr);

  //数组格式对于打包来说不太容易,以下做一个格式的转换
  const graph = {};
  graphArr.forEach((item) => {
    graph[item.filename] = {
      dependencies: item.dependencies,
      code: item.code,
    };
  });

  return graph;
};

const graphInfo = makeDependenciesGraph("./src/index.js"); //1-1、引入入口文件
console.log(graphInfo);

 

三、打包生成代码

依赖图谱分析完毕,实现打包操作,生成代码

const generateCode = (entry) => {
 // const graph = makeDependenciesGraph(entry);  //如下第一个图输出是object有问题
  const graph = JSON.stringify(makeDependenciesGraph(entry));

  //防止污染全局环境,代码需放在闭包中
  return `(function(graph){})(${graph})`;
};

const code = generateCode("./src/index.js"); //1-1、引入入口文件
console.log(code);

   

问题:

  • 如上输出含有require方法和exports对象,这些在浏览器中是不能直接运行的,会报找不到,因此需要构建require方法和exports对象
  return `
    (function(graph){
        function require(module){
            var exports = {}
            return exports
        }

    require(${entry})  //调用构建的require方法
  })(${graph})`;
  • 如上输出require方法中使用的地址是相对路径,但是bundler.js文件位置问题,需要转换为真实路径
//localRequire:根据相对路径拿到模块真实的路径
//function require(module)中的立即执行函数:eval(code); 执行代码,code包含了require, exports的使用,此时require使用的实际是localRequire

//防止污染全局环境,代码需放在闭包中
  return `
  (function (graph) {
    function require(module) {
      function localRequire(relativePath) {
        return require(graph[module].dependencies[relativePath]);
      }

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

      return exports;
    }

    require("${entry}");
  })(${graph})`;

注意:如下运行输出的代码拷贝到浏览器的控制台运行,切记如下直接copy bash中的输出黏贴到浏览器控制台运行可能会出错,因为bash因为宽度限制会断开换行,导致整个json格式中间断开有问题,所以要把上面(${graph})输出的这一段的换行符清掉再运行。

去掉换行符运行:打印出了say hello

猜你喜欢

转载自blog.csdn.net/CamilleZJ/article/details/117172745
今日推荐