webpack必知必会源码基础篇(一)

64 阅读1分钟

自定义loader

function loader(source){
  //console.log('logger1-loader');
  return source+'//logger1';
}
module.exports = loader;

自定义plugins

class RunPlugin{
    apply(compiler){
        compiler.hooks.run.tap('RunPlugin',()=>{
            console.log('run1 开始编译');
        });
    }
}
module.exports = RunPlugin;
const path = require('path');
const RunPlugin = require('./plugins/RunPlugin');
const Run1Plugin = require('./plugins/Run1Plugin');
const Run2Plugin = require('./plugins/Run2Plugin');
const DonePlugin = require('./plugins/DonePlugin');
module.exports = {
    mode:'development',
    //context:process.cwd(),//current working directory
    devtool:false,
    //entry:'./src/entry1.js',//{main:'./src/entry1.js'}
    entry:{
        entry1:'./src/entry1.js',
        entry2:'./src/entry2.js'
    },
    output:{
        path:path.resolve('./dist'),
        filename:'[name].js'
    },
    resolve:{
        //配置查找模块的路径的规则
        //当引入模块的时候,可以不写扩展名
        extensions:['.js','.jsx','.ts','.tsx','.json']
    },
    module:{
        rules:[
            {
                test:/\.js$/,
                use:[
                    path.resolve(__dirname,'loaders/logger1-loader.js'),
                    path.resolve(__dirname,'loaders/logger2-loader.js')
                ]
            }
        ]
    },
    plugins:[
      
        new RunPlugin(),
      
        new Run2Plugin(),
        new Run1Plugin(),
        new DonePlugin(),
    ]
}

webpack入口

const Compiler = require('./Compiler');
function webpack(config){
//1.初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象
 const argv = process.argv.slice(2);//[0node.exe文件路径,1 执行脚本.js,xxxx]
 const shellOptions = argv.reduce((shellOptions,options)=>{
    const [key,value] = options.split('=');
    shellOptions[key.slice(2)]=value
    return shellOptions;
 },{});
 const finalOptions = {...config,...shellOptions};
 //2.用上一步得到的参数初始化 Compiler 对象
 const compiler = new Compiler(finalOptions);
 //3.加载所有配置的插件
 finalOptions.plugins.forEach((plugin)=>{
   plugin.apply(compiler);
 });
 return compiler;
}
module.exports = webpack;

Compiler文件

const {SyncHook} = require('tapable');
const path = require('path');
const Complication = require('./Complication');
const fs = require('fs');
class Compiler{
    constructor(options) {
       this.options = options;
       this.hooks = {
        run:new SyncHook(),//会在开始编译的时候触发
        done:new SyncHook()//会在结束编译的时候触发
       }
    }
    
    run(callback){
        this.hooks.run.call();
        const onCompiled = (err,stats,fileDependencies)=>{
            //10.在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
            const {assets} = stats;
            for(let filename in assets){
                //获取输出文件的绝对路径
                let filePath = path.posix.join(this.options.output.path,filename);
                fs.writeFileSync(filePath,assets[filename],'utf-8');
            }
            callback(err,{
                toJson:()=>stats
            });
            //fileDependencies指的是本次打包涉及哪些文件
            //监听这些文件的变化,当文件发生变化,重新开启一个新的编译
            [...fileDependencies].forEach(file=>{
                fs.watch(file,()=>this.compile(onCompiled));
            });
        }
        //开始一次新的编译
        this.compile(onCompiled);
        this.hooks.done.call();
       
    }
    compile(onCompiled){
        const complication = new Complication(this.options);
        complication.build(onCompiled);
    }
}
module.exports = Compiler;

complication:

const path = require('path');
const fs = require('fs');
const types = require('babel-types');//生成和判断节点的工具库
const parser = require('@babel/parser');//把源代码转换AST的编译器
const traverse = require('@babel/traverse').default;//遍历语法树的工具
const generator = require('@babel/generator').default;//把转换后的语法树重新生成源代码的工具
function toUnixSeq(filePath){
    return filePath.replace(/\\/g,'/');
}
class Complication{
    constructor(options){
       this.options = options;
       this.options.context = this.options.context||toUnixSeq(process.cwd())
       this.fileDependencies = new Set();
       this.modules = [];//存放本次编译所有产生的模块
       this.chunks = [];//存放所有的代码块
       this.assets = {};//存放输出的文件,属性是文件名,值是文件的内容
    }
    build(onCompiled){
        //5.根据配置中的entry找出入口文件
        let entry = {};
        if(typeof this.options.entry === 'string'){
            entry.main = this.options.entry;
        }else{
            entry = this.options.entry;
        }
        for(let entryName in entry){
            //获取入口文件的绝对路径 
            let entryFilePath = path.posix.join(this.options.context,entry[entryName]);
            //把此文件添加到文件依赖列表中
            this.fileDependencies.add(entryFilePath);
            //从入口文件发出,开始编译模块
            let entryModule = this.buildModule(entryName,entryFilePath);
            //8.根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
            let chunk = {
                name:entryName,//入口的名称
                entryModule,//入口的模块 ./src/entry1.js
                modules:this.modules.filter(module=>module.names.includes(entryName))//此入口对应的模块
            }
            this.chunks.push(chunk);
        }
        //9.再把每个 Chunk 转换成一个单独的文件加入到输出列表
        this.chunks.forEach(chunk=>{
          let outputFilename = this.options.output.filename.replace('[name]',chunk.name);
          this.assets[outputFilename] = getSourceCode(chunk);
        });
        onCompiled(
            null,
            {
                modules:this.modules,
                chunks:this.chunks,
                assets:this.assets
            },
            this.fileDependencies
        );
    }
    /**
     * 
     * @param {*} name 入口的名称 main,entry1,entry2
     * @param {*} modulePath 入口模块的文件的绝对路径
     */
    buildModule(entryName,modulePath){
      //从入口文件出发,调用所有配置的Loader对模块进行转换
      let rawSourceCode = fs.readFileSync(modulePath,'utf8');  
      //获取loader的配置规则 
      let {rules} = this.options.module;
      //获取适用于此模块的loader
      let loaders = [];
      rules.forEach(rule=>{
        //用模块路径匹配正则表达式
        if(modulePath.match(rule.test)){
            loaders.push(...rule.use);
        }
      });
      let transformedSourceCode = loaders.reduceRight((sourceCode,loaderPath)=>{
        const loaderFn = require(loaderPath);
        return loaderFn(sourceCode)
      },rawSourceCode);
      //获取当前模块,也就是 ./src/entry1.js的模块ID
      let moduleId = './'+path.posix.relative(this.options.context,modulePath);
      let module = {id:moduleId,names:[entryName],dependencies:new Set()}
      this.modules.push(module);
      //经过loader转换,transformedSourceCode肯定是一个字符串了
      //7.再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
      let ast = parser.parse(transformedSourceCode,{sourceType:"module"});
      traverse(ast,{
        CallExpression:({node})=>{
            //如果调用的方法名是require的话,说明就要依赖一个其它模块
            if(node.callee.name === 'require'){
                // .代表当前的模块所有的目录,不是工作目录
                let depModuleName = node.arguments[0].value;// ./title
                //获取当前的模块所在的目录 C:\7.flow\src
                let dirName = path.posix.dirname(modulePath);
                // C:/7.flow/src/title
                let depModulePath = path.posix.join(dirName,depModuleName);
                let {extensions} = this.options.resolve;
                //尝试添加扩展名找到真正的模块路径
                depModulePath=tryExtensions(depModulePath,extensions);
                //把依赖的模块路径添加到文件依赖列表
                this.fileDependencies.add(depModulePath);
                //获取此模块的ID,也就相对于根目录的相对路径
                let depModuleId = "./"+path.posix.relative(this.options.context,depModulePath)
                //修改语法树,把引入模块路径改为模块的ID
                node.arguments[0]=types.stringLiteral(depModuleId)
                //node.arguments [ { type: 'StringLiteral', value: './src/title.js' } ]
                //给当前的entry1模块添加依赖信息
                module.dependencies.add({depModuleId,depModulePath});
            }
        }
      });
      const {code} = generator(ast);
      //转换源代码,把转换后的源码放在_source属性,用于后面写入文件
      module._source = code;
      [...module.dependencies].forEach(({depModuleId,depModulePath})=>{
        //判断此模块是否已经编译过了,如果编译过了,则不需要重复编译
        let existModule = this.modules.find(item=>item.id === depModuleId);
        if(existModule){//只需要把新的入口名称添回到模块的names数组里就可以
            existModule.names.push(entryName);
        }else{
            this.buildModule(entryName,depModulePath);
            //this.modules.push(depModule);
        }
      });
      return module;
    }
}

function tryExtensions(modulePath,extensions){
    //如果此绝对路径上的文件是真实存在的,直接返回
    if(fs.existsSync(modulePath)){
        return modulePath;
    }
    for(let i=0;i<extensions.length;i++){
        let filePath = modulePath+extensions[i];
        if(fs.existsSync(filePath)){
            return filePath;
        }
    }
    throw new Error(`模块${modulePath}未找到`);
}

function getSourceCode(chunk){
  return `
  (() => {
    var modules = {
      ${
        chunk.modules
        .filter(module=>module.id!==chunk.entryModule.id)
        .map(
            module=>`
            "${module.id}": module => {
               ${module._source}
              }
            `
        )
      }  
    };
    var cache = {};
    function require(moduleId) {
      var cachedModule = cache[moduleId];
      if (cachedModule !== undefined) {
        return cachedModule.exports;
      }
      var module = cache[moduleId] = {
        exports: {}
      };
      modules[moduleId](module, module.exports, require);
      return module.exports;
    }
    var exports = {};
    (() => {
      ${chunk.entryModule._source}
    })();
  })();
  `;
}
module.exports = Complication;

debugger:

const webpack = require('./my-webpack');
const fs = require('fs');
const config = require('./webpack.config');
const compiler = webpack(config);
//4.执行对象的 run 方法开始执行编译
compiler.run((err,stats)=>{
    console.log('====================================');
    console.log(stats.toJson({
        modules:true,//每个文件都是一个模块
        chunks:true,//打印所有的代码块,模块的集合会成一个代码块
        assets:true//输出的文件列表
    }));
    console.log('====================================');
    console.log(err);
    let statsString = JSON.stringify(
        stats.toJson({
            modules:true,//每个文件都是一个模块
            chunks:true,//打印所有的代码块,模块的集合会成一个代码块
            assets:true//输出的文件列表
        })
    );
    fs.writeFileSync('./myStats.json',statsString);
});