webpack-Bundler源码编写(模块分析)

为了简单理解webpack原理。

新建项目:webpack_bundler

// 目录
src
    index.js
    message.js
    word.js
bundler.js

word.js:

export const word=‘hello‘;

message.js:

import {word} from ‘./word.js‘;
    const message=`say ${word}`;
    export default message;

index.js:

import message from ‘./message.js‘;
    console.log(message);

bundler.js:

const fs=require(‘fs‘);
const moduleAnalyser=(filename)=>{
    const content=fs.readFileSync(filename,‘utf-8‘);
    console.log(content);
}

moduleAnalyser(‘./src/index.js‘);

在命令行中执行:node bundler.js,会输出index.js的内容:

import message from ‘./message.js‘;

console.log(message);

我们可以使用babel的一个第三方工具来帮我们分析代码 https://babel.docschina.org/docs/en/babel-parser

npm install @babel/parser --save

修改bundler.js后运行node bundler.js:

const fs=require(‘fs‘);
const parser=require(‘@babel/parser‘);

const moduleAnalyser=(filename)=>{
    const content=fs.readFileSync(filename,‘utf-8‘);
    console.log(parser.parse(content,{
        sourceType:‘module‘ // 当我们使用ES module语法时需要加上
    }));
}

moduleAnalyser(‘./src/index.js‘);

命令行就会输出一个抽象语法树:

Node {
  type: ‘File‘,
  start: 0,
  end: 58,
  loc:
   SourceLocation {
     start: Position { line: 1, column: 0 },
     end: Position { line: 3, column: 21 } },
  errors: [],
  program:  // 表示当前运行的程序
   Node {
     type: ‘Program‘,
     start: 0,
     end: 58,
     loc: SourceLocation { start: [Position], end: [Position] },
     sourceType: ‘module‘,
     interpreter: null,
     body: [ [Node], [Node] ],
     directives: [] },
  comments: [] }

接下来我们看一下program下面的body:

const ast=parser.parse(content,{
    sourceType:‘module‘
});
console.log(ast.program.body);

输出:

[ Node {
    type: ‘ImportDeclaration‘,  // 引入的声明--import message from ‘./message.js‘;
    start: 0,
    end: 35,
    loc: SourceLocation { start: [Position], end: [Position] },
    specifiers: [ [Node] ],
    source:
     Node {
       type: ‘StringLiteral‘,
       start: 20,
       end: 34,
       loc: [SourceLocation],
       extra: [Object],
       value: ‘./message.js‘ } },
  Node {
    type: ‘ExpressionStatement‘,    // 表达式语句--console.log(message);
    start: 37,
    end: 58,
    loc: SourceLocation { start: [Position], end: [Position] },
    expression:
     Node {
       type: ‘CallExpression‘,
       start: 37,
       end: 57,
       loc: [SourceLocation],
       callee: [Node],
       arguments: [Array] } } ]

这样我们就可以拿到所有引入的声明来分析依赖关系,babel还给我们提供了一个模块来帮我们快速找到import节点:

npm install @babel/traverse --save

修改一下bundler.js:

const fs=require(‘fs‘);
const parser=require(‘@babel/parser‘);
const traverse=require(‘@babel/traverse‘).default;

const moduleAnalyser=(filename)=>{
    const content=fs.readFileSync(filename,‘utf-8‘);
    const ast=parser.parse(content,{
        sourceType:‘module‘
    });
    traverse(ast,{
        ImportDeclaration({node}){
            console.log(node);
        }
    });
}

moduleAnalyser(‘./src/index.js‘);

重新运行node bundler.js输出:

Node {
  type: ‘ImportDeclaration‘,
  start: 0,
  end: 35,
  loc:
   SourceLocation {
     start: Position { line: 1, column: 0 },
     end: Position { line: 1, column: 35 } },
  specifiers:
   [ Node {
       type: ‘ImportDefaultSpecifier‘,
       start: 7,
       end: 14,
       loc: [SourceLocation],
       local: [Node] } ],
  source:
   Node {
     type: ‘StringLiteral‘,
     start: 20,
     end: 34,
     loc: SourceLocation { start: [Position], end: [Position] },
     extra: { rawValue: ‘./message.js‘, raw: ‘\‘./message.js\‘‘ },
     value: ‘./message.js‘ } }

这样我们就拿到了所有的依赖,上面的source的value就是我们依赖的文件。 再稍微修改一下bundler.js:

let dependencies=[];
traverse(ast,{
    ImportDeclaration({node}){
        dependencies.push(node.source.value);
    }
});
console.log(dependencies);

会输出:[ ‘./message.js‘ ]

还有一个问题是我们现在拿到的文件路径是相对于src下的路径,我们在开发中需要的是跟目录下的路径,所以还需要做一点改进:

const fs=require(‘fs‘);
const path=require(‘path‘);
const parser=require(‘@babel/parser‘);
const traverse=require(‘@babel/traverse‘).default;

const moduleAnalyser=(filename)=>{
    const content=fs.readFileSync(filename,‘utf-8‘);
    const ast=parser.parse(content,{
        sourceType:‘module‘
    });
    let dependencies=[];
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname=path.dirname(filename);   // ./src
            const newFile=‘./‘+path.join(dirname,node.source.value);
            dependencies.push(newFile);
        }
    });
    console.log(dependencies);
}

moduleAnalyser(‘./src/index.js‘);

命令行输出:[ ‘./src/message.js‘ ]

但是如果我们只存绝对路径二没有相对路径的话最后打包还是会比较麻烦,所以最好是把两个路径都存下来:

let dependencies={};
traverse(ast,{
    ImportDeclaration({node}){
        const dirname=path.dirname(filename);
        const newFile=path.join(dirname,node.source.value);
        dependencies[node.source.value]=newFile;
    }
});
console.log(dependencies);

命令行输出:{ ‘./message.js‘: ‘src/message.js‘ }

这就是我们想要的结果,最后只要返回就可以了: return { filename,dependencies }

浏览器无法运行这样的代码,所以我们还需要将它转换成浏览器可以运行的代码,我们还可以借助babel的一个模块:

npm install @babel/core --save

还需要转换成es5:

npm install @babel/preset-env --save

修改bundler.js:

const fs=require(‘fs‘);
const path=require(‘path‘);
const parser=require(‘@babel/parser‘);
const traverse=require(‘@babel/traverse‘).default;
const babel=require(‘@babel/core‘);

const moduleAnalyser=(filename)=>{
    const content=fs.readFileSync(filename,‘utf-8‘);
    const ast=parser.parse(content,{
        sourceType:‘module‘
    });
    let dependencies={};
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname=path.dirname(filename);
            const newFile=path.join(dirname,node.source.value);
            dependencies[node.source.value]=newFile;
        }
    });
    const {code}=babel.transformFromAst(ast,null,{
        presets:[‘@babel/preset-env‘]
    });
    console.log(code);  // 浏览器可以运行的代码
    return {
        filename,dependencies,code
    }
}

moduleAnalyser(‘./src/index.js‘);

命令行输出:

"use strict";

var _message = _interopRequireDefault(require("./message.js"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

console.log(_message["default"]);

这就转换成浏览器可以运行的代码了。