【webpack】简单模拟实现

webpack | 2020-05-15 10:59:25 387次 2次

一、准备

1、初始化 package.json

{
  "name": "supack",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "bin": {
      // bin的作用是 可以直接运行的命令 比如  wex 
    "supack": "./bin/supack.js"  
  }
}

2、创建目录

supack--根目录
    bin -- 入口目录
        supack.js --入口
    lib -- 业务逻辑目录
        compiler.js -- 编译主逻辑
        temp.ejs    -- 生成目标文件(编译后文件)

3、运行

supack 丢到待编译项目的  node_modules目录下,然后执行 npx supack 命令执行。

更好的一个方式是将 supack 目录单独出来,通过 npm link 关联到全局,然后在开发项目中通过 npm link supack 关联上 supack,这样在待编译项目中的 node_module 中会被创建一份 supack 出来,而且抽离出来的 supack 修改后,待编译项目中 node_module 下的 supack 也会随着修改,这样做的好处是可以关联多个项目。

执行 npx 的时候,会去找待编译项目中 node_module / .bin目录,如果对应的包不存在的话,就会自动下载,并创建.cmd文件,然后自动找到刚创建的包里面对应模块的bin目录,并执行入口文件

@ECHO off
SETLOCAL
CALL :find_dp0

IF EXIST "%dp0%\node.exe" (
  SET "_prog=%dp0%\node.exe"
) ELSE (
  SET "_prog=node"
  SET PATHEXT=%PATHEXT:;.JS;=;%
)

"%_prog%"  "%dp0%\..\supack\bin\supack.js" %*
ENDLOCAL
EXIT /b %errorlevel%
:find_dp0
SET dp0=%~dp0
EXIT /b



二、编写

1、supack.js

#! /usr/bin/env node

//打印 版本信息或者其他帮助命令
function run (argv) {
    if (argv[0] === '-v' || argv[0] === '--version') {
        console.log('  version is 0.0.1');
    } else if (argv[0] === '-h' || argv[0] === '--help') {
        console.log('  usage:\n');
    }
}
run(process.argv.slice(2));

//get webpack.config.js
let path = require('path');
let Compiler = require("../lib/compiler");
//config 配置文件
let config = require(path.resolve('webpack.config.js'));
console.log('11111111111111112222222222222')
let compiler = new Compiler(config);
//执行钩子
compiler.hooks.entryOption.call()
compiler.run()

2、compiler.js

let fs = require('fs');
let path = require('path');

//babylon 源码 ---> ast
//@babel/traverse  遍历节点
//@babel/types     替换节点
//@babel/generator 替换后的结果生成
let babylon = require('babylon');
let traverse = require('@babel/traverse').default;
let generator = require('@babel/generator').default;
let t = require('@babel/types');

let ejs = require('ejs');

let { SyncHook } = require('tapable');

class Compiler{
  constructor(config) {
    this.config = config;
    //'./src/index.js'
    this.entryId;
    this.modules = {};
    // 入口文件
    this.entry = config.entry;
    //工作路径
    this.root = process.cwd();
    //钩子
    this.hooks = {
      entryOption: new SyncHook(),
      compile: new SyncHook(),
      afterCompile: new SyncHook(),
      afterPulgins: new SyncHook(),
      run: new SyncHook(),
      emit: new SyncHook(),
      done: new SyncHook(),
    }
    //如果传递了 插件 参数
    let plugins = this.config.plugins;
    if(Array.isArray(plugins)){
      plugins.forEach(item => {
        //插件中的方法  不是改变指向
        item.apply(this)
      })
    }
    this.hooks.afterPulgins.call()
  }
  getSource(modulePath){
    let content = fs.readFileSync(modulePath, 'utf-8');
    //加载loader
    let rules = this.config.module.rules;
    for(let i = 0, len = rules.length; i < len; i++){
      let rule = rules[i],
        { test, use } = rule,
        len = use.length - 1;
      //匹配指定文件
      if(test.test(modulePath)){
        //获取对应loader函数
        function normalLoader(){
          let loader = require(use[len--]);
          content = loader(content)
          if(len >= 0){
            normalLoader()
          }
        }
        normalLoader()
      }
    }
    return content;
  }
  //解析源码 AST解析语法树
  parse(source, parentPath){
    let ast = babylon.parse(source);
    let dependencies = [];
    traverse(ast, {
      CallExpression(p){
        let node = p.node; //对应节点
        if(node.callee.name == "require"){
          node.callee.name = '__webpack_require__';
          let moduleName = node.arguments[0].value;
          moduleName = moduleName + (path.extname(moduleName) ? "" : ".js");
          moduleName = './' + path.join(parentPath, moduleName);
          dependencies.push(moduleName);
          node.arguments = [t.stringLiteral(moduleName)];
        }
      }
    })
    let sourceCode = generator(ast).code;
    return {
      sourceCode,
      dependencies
    }
  }
  //构建模块
  buildModule(modulePath, isEntry){
    //get  模块内容 Id
    let source = this.getSource(modulePath)
    let moduleName = './' + path.relative(this.root, modulePath);
    //保存入口名字
    if(isEntry){
      this.entryId = moduleName;
    }
    //创建依赖列表
    let {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName));
    this.modules[moduleName] = sourceCode;
    //父模块之外的模块 递归加载
    dependencies.forEach(item => {
      this.buildModule(path.join(this.root, item), false)
    })
  }
  emitFile(){
    //输出路径
    let main = path.join(this.config.output.path, this.config.output.filename);
    //模板
    let templateStr = this.getSource(path.join(__dirname, 'main.ejs'));
    let code = ejs.render(templateStr, {
      entryId: this.entryId,
      modules: this.modules
    })
    this.assets = {}
    this.assets[main] = code
    fs.writeFileSync(main, this.assets[main])
  }
  //执行
  run(){
    this.hooks.run.call()
    this.hooks.compile.call()
    // 创建模块依赖关系
    this.buildModule(path.resolve(this.root, this.entry), true);
    this.hooks.afterCompile.call()
    //发送打包后的文件
    this.emitFile();
    this.hooks.emit.call()
    this.hooks.done.call()
  }
}

module.exports = Compiler;

3、temp.ejs

(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;
  }
  return __webpack_require__(__webpack_require__.s = "<%-entryId%>");
  })({ 
  <%for(let key in  modules){%>
    "<%-key%>":(function(module, exports, __webpack_require__){
      eval(`<%-modules[key]%>`);
    }),
  <%}%>
});



2人赞

分享到: