4. 5.Webpack4-webpack手写

135 阅读9分钟

三十二、1.webpack手写

  1. npx webpack原理
  • 新建文件夹kft-pack,初始化npm init -y,package.json添加"bin": {"kft-pack": "./bin/kft-pack.js"},,kft-pack下新建bin文件夹,bin下新建kft-pack.js文件,添加#! /usr/bin/env node,另起一行console.log('hello,world!');,命令行执行npm link,连接到全局 *. 新建webpack-dev,文件下执行npm link kft-pack,命令连到本文件下,执行npx kft-pack
  1. webpack-dev
  • src/index.js let str = require('./a');console.log(str);
  • src/a.js let b = require('./base/b');module.exports = 'a' + b;
  • src/base/b.js module.exports = 'b';
  • node执行index.js,打印ab
  • npx webpack 打包

三十三、2.webpack分析及处理

  1. kft-pack文件夹
  • bin/kft-pack.js
#! /usr/bin/env node

// 1)需要找到当前执行名的路径,拿到webpack.config.js
let path = require('path');
// configp配置文件
let config = require(path.resolve('webpack.config.js'));
// 编译
let Compiler = require('../lib/Compiler.js');
let compiler = new Compiler(config);
compiler.run();
  • bin/lib/Compiler.js
class Compiler {
  constructor(config) {
    // 输入输出配置
    this.config = config;
    // 需要保存入口文件路径
    this.entryId; // './src/index.js'
    // 需要保存所有的模块依赖
    this.modules = {};
    this.entry = config.entry; // 入口路径
    // 工作路径
    this.root = process.cwd();
  }
  buildModule(modulePath,isEntry) {
  }
  emitFile() { // 发射文件
  }
  run() {
    // 执行并且创建模块的依赖关系
    this.buildModule(path.resolve(this.root, this.entry), true);
    // 发射一个文件,打包后的文件
    this.emitFile();
  }
}

三十四、3.创建依赖关系

  • bin/lib/Compiler.js
let path = require('path');
let fs = require('fs');
class Compiler {
  constructor(config) {
    // 输入输出配置
    this.config = config;
    // 需要保存入口文件路径
    this.entryId; // './src/index.js'
    // 需要保存所有的模块依赖
    this.modules = {};
    this.entry = config.entry; // 入口路径
    // 工作路径
    this.root = process.cwd();
  }
  getSource(modulePath) { // #3. 公共方法:获取读取的文件内容
    let content = fs.readFileSync(modulePath, 'utf8');
    return content;
  }
  parse(source, parentPath) { // #9. 解析源码 AST解析语法树
    // #10. 打印查看
    // console.log(source, parentPath)
  }
  buildModule(modulePath, isEntry) { // #1. 构建模块
    // #2. 拿到模块内容
    let source = this.getSource(modulePath);
    // #4. 模块id modulePath = modulePath-this.root src/index.js
    let moduleName = './' + path.relative(this.root, modulePath);
    // #8. 保存入口的名字
    if (isEntry) {
      this.entryId = moduleName;
    }
    // #5. 打印查看对错
    // console.log(source, moduleName);
    // #6. 解析,需要把source源码进行改造,返回一个依赖列表
    let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName));
    // #7. 把相对路径和模块中的内容一一对应起来
    this.modules[moduleName] = sourceCode;
  }
  emitFile() { // 发射文件
  }
  run() {
    // 执行并且创建模块的依赖关系
    this.buildModule(path.resolve(this.root, this.entry), true);
    // 发射一个文件,打包后的文件
    this.emitFile();
  }
}
module.exports = Compiler;

三十五、4.AST递归解析:给名字,加源码

  1. babylon 源码转换成AST ast官网
  2. 安装包:babylon @babel/traverse @babel/types @babel/generator
  3. Compiler.js
let path = require('path');
let fs = require('fs');
// #1. babylon @babel/traverse @babel/types @babel/generator
let babylon = require('babylon');
let traverse = require('@babel/traverse').default;
let t = require('@babel/types');
let generator = require('@babel/generator').default;
class Compiler {
  constructor(config) {
    // 输入输出配置
    this.config = config;
    // 需要保存入口文件路径
    this.entryId; // './src/index.js'
    // 需要保存所有的模块依赖
    this.modules = {};
    this.entry = config.entry; // 入口路径
    // 工作路径
    this.root = process.cwd();
  }
  getSource(modulePath) { // 公共方法:获取读取的文件内容
    let content = fs.readFileSync(modulePath, 'utf8');
    return content;
  }
  parse(source, parentPath) { // 解析源码 AST解析语法树
    // 打印查看
    // console.log(source, parentPath)
    // #2. babylon解析
    let ast = babylon.parse(source);
    let dependencies = []; // #9. 依赖数组
    traverse(ast, { // #3. 重构ast
      CallExpression(p) { // #4. a() require()
        let node = p.node; // #5. 对应的节点
        if (node.callee.name === 'require') {
          node.callee.name = '__webpack_require__'; // #6. 修改require名
          let moduleName = node.arguments[0].value; // #7. 取到是模块的引用名字
          moduleName = moduleName + (path.extname(moduleName) ? '' : '.js'); // #8. 添加扩展名
          moduleName = './' + path.join(parentPath, moduleName); // #8. 拼接附路径 './src/a.js'
          dependencies.push(moduleName); // #9. 添加到依赖数组
          node.arguments = [t.stringLiteral(moduleName)];
        }
      }
    })
    let sourceCode = generator(ast).code; // #10. 重新生成一下
    return { sourceCode, dependencies }
  }
  buildModule(modulePath, isEntry) { // 构建模块
    // 拿到模块内容
    let source = this.getSource(modulePath);
    // 模块id modulePath = modulePath-this.root src/index.js
    let moduleName = './' + path.relative(this.root, modulePath);
    // 保存入口的名字
    if (isEntry) {
      this.entryId = moduleName;
    }
    // 打印查看对错
    // console.log(source, moduleName);
    // 解析,需要把source源码进行改造,返回一个依赖列表
    let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName));
    // #11. 打印
    // console.log(sourceCode, dependencies)
    // 把相对路径和模块中的内容一一对应起来
    this.modules[moduleName] = sourceCode;
    // #12. 所有依赖项进行递归
    dependencies.forEach(dep => {
      this.buildModule(path.join(this.root, dep), false); // #13. 附模块的加载
    })
  }
  emitFile() { // 发射文件
    // #13. 用数据渲染模板
  }
  run() {
    // 执行并且创建模块的依赖关系
    this.buildModule(path.resolve(this.root, this.entry), true);
    // #13. 打印
    console.log(this.modules, this.entryId);
    // 发射一个文件,打包后的文件
    this.emitFile();
  }
}
module.exports = Compiler;

三十六、5.生成打包结果

  1. bin/lib/Compiler.js
let path = require('path');
let fs = require('fs');
// babylon @babel/traverse @babel/types @babel/generator
let babylon = require('babylon');
let traverse = require('@babel/traverse').default;
let t = require('@babel/types');
let generator = require('@babel/generator').default;
let ejs = require('ejs');
class Compiler {
  constructor(config) {
    // 输入输出配置
    this.config = config;
    // 需要保存入口文件路径
    this.entryId; // './src/index.js'
    // 需要保存所有的模块依赖
    this.modules = {};
    this.entry = config.entry; // 入口路径
    // 工作路径
    this.root = process.cwd();
  }
  getSource(modulePath) { // 公共方法:获取读取的文件内容
    let content = fs.readFileSync(modulePath, 'utf8');
    return content;
  }
  parse(source, parentPath) { // 解析源码 AST解析语法树
    // 打印查看
    // console.log(source, parentPath)
    // babylon解析
    let ast = babylon.parse(source);
    let dependencies = []; // 依赖数组
    traverse(ast, { // 重构ast
      CallExpression(p) { // #4. a() require()
        let node = p.node; // #5. 对应的节点
        if (node.callee.name === 'require') {
          node.callee.name = '__webpack_require__'; // 修改require名
          let moduleName = node.arguments[0].value; // 取到是模块的引用名字
          moduleName = moduleName + (path.extname(moduleName) ? '' : '.js'); // #8. 添加扩展名
          moduleName = './' + path.join(parentPath, moduleName); // #8. 拼接附路径 './src/a.js'
          dependencies.push(moduleName); // 添加到依赖数组
          node.arguments = [t.stringLiteral(moduleName)];
        }
      }
    })
    let sourceCode = generator(ast).code; //  重新生成一下
    return { sourceCode, dependencies }
  }
  buildModule(modulePath, isEntry) { // 构建模块
    // 拿到模块内容
    let source = this.getSource(modulePath);
    // 模块id modulePath = modulePath-this.root src/index.js
    let moduleName = './' + path.relative(this.root, modulePath);
    // 保存入口的名字
    if (isEntry) {
      this.entryId = moduleName;
    }
    // 打印查看对错
    // console.log(source, moduleName);
    // 解析,需要把source源码进行改造,返回一个依赖列表
    let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName));
    //  打印
    // console.log(sourceCode, dependencies)
    // 把相对路径和模块中的内容一一对应起来
    this.modules[moduleName] = sourceCode;
    //  所有依赖项进行递归
    dependencies.forEach(dep => {
      this.buildModule(path.join(this.root, dep), false); //  附模块的加载
    })
  }
  emitFile() { // 发射文件
    //  用数据渲染模板
    // #1. 输出的目录
    let main = path.join(this.config.output.path, this.config.output.filename);
    // #3. 模板路径
    let templateStr = this.getSource(path.join(__dirname, 'main.ejs'));
    // #4. ejs渲染
    let code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules })
    // #2. 存放的资源
    this.assets = {};
    // #5. 资源中路径对应的代码
    this.assets[main] = code;
    // #6. 本来需要遍历插入,为了方便,直接用writeFileSync
    fs.writeFileSync(main, this.assets[main]);
  }
  run() {
    // 执行并且创建模块的依赖关系
    this.buildModule(path.resolve(this.root, this.entry), true);
    // 打印
    // console.log(this.modules, this.entryId);
    // 发射一个文件,打包后的文件
    this.emitFile();
  }
}
module.exports = Compiler;
  • bin/lib/main.ejs
(function (modules) { // webpackBootstrap
  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]%>`);
      }),
    <%}%>
  });

三十七、6.增加loader

  1. webpack-dev
  • webpack.config.js
let path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.less/,
        use: [
          path.resolve(__dirname, 'loader', 'style-loader'),
          path.resolve(__dirname, 'loader', 'less-loader')
        ]
      }
    ]
  }
}
  • 新建loader文件夹,loader/less-loader.js loader/style-loader.js
  • 安装包 less
  • loader/less-loader.js
let less = require('less');
function loader(source) {
  let css = '';
  less.render(source, function (err, c) {
    css = c.css;
  })
  css = css.replace(/\n/g, '\\n'); // 为了浏览器可以解析断行
  return css;
}
module.exports = loader;
  • loader/style-loader.js
function loader(source) {
  let style = `
    let style = document.createElement('style');
    style.innerHTML = ${JSON.stringify(source)}
    document.head.appendChild(style);
  `;
  return style;
}
module.exports = loader;
  1. kft-pack文件夹
  • bin/lib/Compiler.js
let path = require('path');
let fs = require('fs');
// babylon @babel/traverse @babel/types @babel/generator
let babylon = require('babylon');
let traverse = require('@babel/traverse').default;
let t = require('@babel/types');
let generator = require('@babel/generator').default;
let ejs = require('ejs');
class Compiler {
  constructor(config) {
    // 输入输出配置
    this.config = config;
    // 需要保存入口文件路径
    this.entryId; // './src/index.js'
    // 需要保存所有的模块依赖
    this.modules = {};
    this.entry = config.entry; // 入口路径
    // 工作路径
    this.root = process.cwd();
  }
  getSource(modulePath) { // 公共方法:获取读取的文件内容
    // #1. 拿到所有规则
    let rules = this.config.module.rules;
    let content = fs.readFileSync(modulePath, 'utf8');
    for (let i = 0; i < rules.length; i++) {
      let rule = rules[i];
      let { test, use } = rule;
      let len = use.length - 1;
      if (test.test(modulePath)) { // #2. 这个模块需要loader来转化
        // #3.  loader 获取对应的loader函数,递归处理
        function normalLoader() {
          let loader = require(use[len--]);
          content = loader(content);
          if (len >= 0) {
            normalLoader();
          }
        }
        normalLoader();
      }
    }
    return content;
  }
  parse(source, parentPath) { // 解析源码 AST解析语法树
    // 打印查看
    // console.log(source, parentPath)
    // babylon解析
    let ast = babylon.parse(source);
    let dependencies = []; // 依赖数组
    traverse(ast, { // 重构ast
      CallExpression(p) { //  a() require()
        let node = p.node; //  对应的节点
        if (node.callee.name === 'require') {
          node.callee.name = '__webpack_require__'; // 修改require名
          let moduleName = node.arguments[0].value; // 取到是模块的引用名字
          moduleName = moduleName + (path.extname(moduleName) ? '' : '.js'); //  添加扩展名
          moduleName = './' + path.join(parentPath, moduleName); //  拼接附路径 './src/a.js'
          dependencies.push(moduleName); // 添加到依赖数组
          node.arguments = [t.stringLiteral(moduleName)];
        }
      }
    })
    let sourceCode = generator(ast).code; //  重新生成一下
    return { sourceCode, dependencies }
  }
  buildModule(modulePath, isEntry) { // 构建模块
    // 拿到模块内容
    let source = this.getSource(modulePath);
    // 模块id modulePath = modulePath-this.root src/index.js
    let moduleName = './' + path.relative(this.root, modulePath);
    // 保存入口的名字
    if (isEntry) {
      this.entryId = moduleName;
    }
    // 打印查看对错
    // console.log(source, moduleName);
    // 解析,需要把source源码进行改造,返回一个依赖列表
    let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName));
    //  打印
    // console.log(sourceCode, dependencies)
    // 把相对路径和模块中的内容一一对应起来
    this.modules[moduleName] = sourceCode;
    //  所有依赖项进行递归
    dependencies.forEach(dep => {
      this.buildModule(path.join(this.root, dep), false); //  附模块的加载
    })
  }
  emitFile() { // 发射文件
    //  用数据渲染模板
    //  输出的目录
    let main = path.join(this.config.output.path, this.config.output.filename);
    //  模板路径
    let templateStr = this.getSource(path.join(__dirname, 'main.ejs'));
    //  ejs渲染
    let code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules })
    //  存放的资源
    this.assets = {};
    //  资源中路径对应的代码
    this.assets[main] = code;
    //  本来需要遍历插入,为了方便,直接用writeFileSync
    fs.writeFileSync(main, this.assets[main]);
  }
  run() {
    // 执行并且创建模块的依赖关系
    this.buildModule(path.resolve(this.root, this.entry), true);
    // 打印
    // console.log(this.modules, this.entryId);
    // 发射一个文件,打包后的文件
    this.emitFile();
  }
}
module.exports = Compiler;

三十八、7.增加plugins

  1. kft-pack
  • 安装包 tapable 使用钩子
  • bin/lib/Compiler.js
let path = require('path');
let fs = require('fs');
// babylon @babel/traverse @babel/types @babel/generator
let babylon = require('babylon');
let traverse = require('@babel/traverse').default;
let t = require('@babel/types');
let generator = require('@babel/generator').default;
let ejs = require('ejs');
// #1. 引入钩子
let { SyncHook } = require('tapable');
class Compiler {
  constructor(config) {
    // 输入输出配置
    this.config = config;
    // 需要保存入口文件路径
    this.entryId; // './src/index.js'
    // 需要保存所有的模块依赖
    this.modules = {};
    this.entry = config.entry; // 入口路径
    // 工作路径
    this.root = process.cwd();
    // #2. 在生命周期使用钩子
    this.hooks = {
      // 入口
      entryOptions: new SyncHook(),
      // 编译
      compile: new SyncHook(),
      // 编译后
      afterCompile: new SyncHook(),
      // 引完插件后
      afterPlugins: new SyncHook(),
      // 运行
      run: new SyncHook(),
      // 发送
      emit: new SyncHook(),
      // 完成
      done: new SyncHook(),
    }
    // #3. 如果传递了plugins参数
    let plugins = this.config.plugins;
    if (Array.isArray(plugins)) {
      plugins.forEach(plugin => {
        plugin.apply(this);
      })
      this.hooks.afterPlugins.call(); // #
    }
  }
  getSource(modulePath) { // 公共方法:获取读取的文件内容
    // 拿到所有规则
    let rules = this.config.module.rules;
    let content = fs.readFileSync(modulePath, 'utf8');
    for (let i = 0; i < rules.length; i++) {
      let rule = rules[i];
      let { test, use } = rule;
      let len = use.length - 1;
      if (test.test(modulePath)) { //  这个模块需要loader来转化
        // loader 获取对应的loader函数
        function normalLoader() {
          let loader = require(use[len--]);
          content = loader(content);
          if (len >= 0) {
            normalLoader();
          }
        }
        normalLoader();
      }
    }
    return content;
  }
  parse(source, parentPath) { // 解析源码 AST解析语法树
    // 打印查看
    // console.log(source, parentPath)
    // babylon解析
    let ast = babylon.parse(source);
    let dependencies = []; // 依赖数组
    traverse(ast, { // 重构ast
      CallExpression(p) { //  a() require()
        let node = p.node; //  对应的节点
        if (node.callee.name === 'require') {
          node.callee.name = '__webpack_require__'; // 修改require名
          let moduleName = node.arguments[0].value; // 取到是模块的引用名字
          moduleName = moduleName + (path.extname(moduleName) ? '' : '.js'); //  添加扩展名
          moduleName = './' + path.join(parentPath, moduleName); //  拼接附路径 './src/a.js'
          dependencies.push(moduleName); // 添加到依赖数组
          node.arguments = [t.stringLiteral(moduleName)];
        }
      }
    })
    let sourceCode = generator(ast).code; //  重新生成一下
    return { sourceCode, dependencies }
  }
  buildModule(modulePath, isEntry) { // 构建模块
    // 拿到模块内容
    let source = this.getSource(modulePath);
    // 模块id modulePath = modulePath-this.root src/index.js
    let moduleName = './' + path.relative(this.root, modulePath);
    // 保存入口的名字
    if (isEntry) {
      this.entryId = moduleName;
    }
    // 打印查看对错
    // console.log(source, moduleName);
    // 解析,需要把source源码进行改造,返回一个依赖列表
    let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName));
    //  打印
    // console.log(sourceCode, dependencies)
    // 把相对路径和模块中的内容一一对应起来
    this.modules[moduleName] = sourceCode;
    //  所有依赖项进行递归
    dependencies.forEach(dep => {
      this.buildModule(path.join(this.root, dep), false); //  附模块的加载
    })
  }
  emitFile() { // 发射文件
    //  用数据渲染模板
    //  输出的目录
    let main = path.join(this.config.output.path, this.config.output.filename);
    //  模板路径
    let templateStr = this.getSource(path.join(__dirname, 'main.ejs'));
    //  ejs渲染
    let code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules })
    //  存放的资源
    this.assets = {};
    //  资源中路径对应的代码
    this.assets[main] = code;
    //  本来需要遍历插入,为了方便,直接用writeFileSync
    fs.writeFileSync(main, this.assets[main]);
  }
  run() {
    this.hooks.run.call(); // #
    this.hooks.compile.call(); // #
    // 执行并且创建模块的依赖关系
    this.buildModule(path.resolve(this.root, this.entry), true);
    // 打印
    // console.log(this.modules, this.entryId);
    this.hooks.afterCompile.call(); // #
    // 发射一个文件,打包后的文件
    this.emitFile();
    this.hooks.emit.call(); // #
    this.hooks.done.call(); // #
  }
}
module.exports = Compiler;
  • bin/kft-pack.js
#! /usr/bin/env node

let fs = require('fs');
// 1)需要找到当前执行名的路径,拿到webpack.config.js
let path = require('path');
// configp配置文件
let config = require(path.resolve('webpack.config.js'));
// 编译
let Compiler = require('./lib/Compiler.js');
let compiler = new Compiler(config);
compiler.hooks.entryOptions.call(); // #
compiler.run();
  1. webpack-dev
  • webpack.config.js
let path = require('path');
class P{
  apply(compiler){
    compiler.hooks.emit.tap('emit',function(){
      console.log('emit-------------');
    })
  }
}
class P1{
  apply(compiler){
    compiler.hooks.afterCompile.tap('emit',function(){
      console.log('afterCompile-------------');
    })
  }
}
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.less/,
        use: [
          path.resolve(__dirname, 'loader', 'style-loader'),
          path.resolve(__dirname, 'loader', 'less-loader')
        ]
      }
    ]
  },
  plugins: [
    new P(),
    new P1(),
  ]
}