最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 手写一个简单的 webpack 编译代码

    正文概述 掘金(天天晒网的渔夫)   2020-12-24   446

    一、webpack 打包编译的主要流程

    compiler 的流程:
    1. 将 webpack.config.js 作为参数传入 Compiler 类 (entry-options)
    2. 创建 Compiler 实例
    3. 调用 Compiler.run 开始编译 (make)
    4. 创建 Compilation( compiler 内创建 compilation 对象,并将 this 传入,compilation 就包含了对 compiler 的引用)
    5. 基于配置开始创建 Chunk (读取文件,转成 AST )
    6. 使用 Parser 从 Chunk 开始解析依赖 (找到依赖关系)
    7. 使用 Module 和 Dependency 管理代码模块相互依赖关系 (build-module)
    8. 使用 Template 基于 Compilation 的数据生成结果代码
    • 可以简单分为这三个阶段
    手写一个简单的 webpack 编译代码

    二、准备工作

    我们先建一个项目,目录如下:

      selfWebpack
        - src
          - data.js
          - index.js
          - random.js
    
    // index.js
    import data from './data.js'
    import random from './random.js'
    
    console.log('?我是数据文件--->', data)
    console.log('?我是随机数--->', random)
    console.log('?我是index.js')
    
    // data.js
    const result = '我是文件里面的数据'
    
    export default result
    
    // random.js
    const random = Math.random()
    
    export default random
    
    

    然后我们先用 webpack 进行一次打包,分析一下 我们需要做什么工作

    // 基本安装
    npm init -y
    npm install webpack@4.44.2 webpack-cli@4.2.0 --save-dev
    
    // package.json
    // 修改
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "build": "webpack --mode development"
    },
    

    整理一下打包后的代码

    (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;
      }
      // Load entry module and return exports
      return __webpack_require__(__webpack_require__.s = "./src/index.js");
    })({
      "./src/data.js": function(module, __webpack_exports__, __webpack_require__) {
        "use strict";
        const result = '我是文件里面的数据'
        __webpack_exports__["default"] = (result);
    
      },
      "./src/index.js": function(module, __webpack_exports__, __webpack_require__) {
        "use strict";
        var _random_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./src/random.js");
        console.log('?我是数据文件--->', _data_js__WEBPACK_IMPORTED_MODULE_0__["default"])
        console.log('?我是随机数--->', _random_js__WEBPACK_IMPORTED_MODULE_1__["default"])
        console.log('?我是index.js')
      },
      "./src/random.js": function(module, __webpack_exports__, __webpack_require__) {
        "use strict";
        const random = Math.random()
        __webpack_exports__["default"] = (random);
      }
    });
    

    最外层是一个立即执行函数,入参是所有的 modules(模块) list。传入的 modules 参数是一个对象。

    • 对象的格式是,文件名: 方法。
    • key 是 index.js 文件的相对路径,value 是一个匿名函数,函数体里面就是咱们写在 index.js 里的代码。(这就是 webpack 加载模块的方式)
    我们要是实现的两个功能
    1. import 变成 __webpack_require__
    2. 读取模块中的所有依赖,生成一个 Template

    三、开始搭建自己的 selfpack

    • 实现 打包编译的代码,放在 src 同级的 selfpack 目录,再增加一个配置文件(selfpack.config.js),如下:
      selfWbpack
        + src
        // 新增
        - selfpack
          - compilation.js
          - compiler.js
          - index.js
          - Parser.js
        - selfpack.config.js
    
    // selfpack.config.js
    const { join } = require('path')
    module.exports = {
      entry: join(__dirname, './src/index.js'),
      output: {
        path: join(__dirname, './dist'),
        filename: 'main.js'
      }
    }
    

    四、实现转换 AST

    • 为什么要转成 ast ? 因为有 import ,我们要把它替换成 webpack_require 。
    • 怎么做? 遍历 AST ,把其中 import 语句引入的文件路径收集起来。
    1. 第一步,实现通过参数找到入口文件并获取文件内容
    2. 第二步,转成 AST
    3. 第三步,解析主模块文件依赖
    4. 第四步,将 AST 转换回 JS 代码
    5. 第五步,分析模块之间的依赖关系,将 import 替换成 webpack_require
    4.1 获取入口文件
    // selfpack/index.js
    const Compiler = require('./Compiler')
    const options = require('../selfpack.config.js')
    const compiler = new Compiler(options)
    compiler.run()
    
    // selfpack/compilation.js
    const fs = require('fs')
    
    class Compilation {
      constructor(compiler) {
        const { options } = compiler
        this.options = options
      }compiler
      static ast(path){
        const content = fs.readFileSync(path, 'utf-8') // 读取文件
        console.log('获取文件', content)
      }
      buildModule(absolutePath, isEntry) {
        this.ast(absolutePath)
      }
    }
    module.exports = Compilation
    

    npm install tapable

    // selfpack/compiler.js
    const { SyncHook } = require('tapable')
    const Compilation = require('./Compilation')
    
    class Compiler {
      constructor(options) {
        this.options = options
        this.hooks = {
          run: new SyncHook()
        }
      }
      run() {
        this.compile()
      }
      compile() {
        const compilation = new Compilation(this)
         //通过entry找入口文件
         const entryModule = compilation.buildModule(this.options.entry, true)
      }
    }
    module.exports = Compiler
    

    静态方法 MDN

    // selfpack/Parser.js
    const fs = require('fs')
    class Parser{
      static ast(path) {
        const content = fs.readFileSync(path, 'utf-8') // 读取文件
        console.log('读取文件', content)
      }
    }
    module.exports = Parser
    

    将 selfpack.config.js 作为参数传入 Compiler 类,执行 run 方法。 通过 new 一个 Compilation 实例,调用 buildModule()

    • buildModule( absolutePath, isEntry )
      • absolutePath: 入口文件的绝对路径
      • isEntry: 是否是主模块

    获取入口文件的结果:

    手写一个简单的 webpack 编译代码

    第一步成功实现,下面实现第二步转成AST

    4.2 转化成AST

    这一步需要用到 @babel/parser , 将代码转化为 AST 语法树。
    npm install @babel/parser sourceType 代表我们要解析的是ES模块

    • 调用 Parser.ast()
    • 通过 readFileSync 读取文件内容,传给 parser.parse() 得到 AST。
    // selfpack/Parser.js
    const fs = require('fs')
    const parser = require('@babel/parser')
    
    class Parser{
      static ast(path) {
        const content = fs.readFileSync(path, 'utf-8') // 读取文件
        console.log('读取文件', content)
        const _ast = parser.parse(content, {
          sourceType: 'module' //表示我们要解析的是ES模块
        })
        console.log(_ast)
        console.log('我是body内容', _ast.program.body)
        return _ast
      }
    }
    module.exports = Parser
    
    手写一个简单的 webpack 编译代码

    到这一步我们很顺利! 这是整个文件的信息,而我们需要的文件内容在它的属性 program 里的 body 里。 看一下 body 的内容

    手写一个简单的 webpack 编译代码

    这是 src/index.js 的一个 import 的 Node 属性,它的类型是 ImportDeclaration。

    4.3 解析主模块文件依赖

    接下来,解析主模块。

    遍历AST要用到 @babel/traverse
    npm install @babel/traverse
    traverse() 的用法:第一个参数就是 AST ,第二个参数就是配置对象

    // selfpack/Parser.js
    const traverse  = require('@babel-traverse').default
    const fs = require('fs')
    const parser = require('@babel/parser')
    const path = require('path')
    
    class Parser{
      static ast(path){
        const content = fs.readFileSync(path, 'utf-8') // 读取文件
        const _ast = parser.parse(content, {
          sourceType: 'module' //表示我们要解析的是ES模块
        })
        console.log(_ast)
        console.log('我是body内容', _ast.program.body)
        return _ast
      }
      static getDependecy(ast, file) {
        const dependecies = {}
        traverse(ast, {
          ImportDeclaration: ({node}) => {
            const oldValue = node.source.value
            const dirname = path.dirname(file)
            const relativepath = "./" + path.join(dirname, oldValue) 
            dependecies[oldValue] = relativepath
            node.source.value = relativepath // 将 ./data.js 转化成 ./src/data.js
          }
        })
        return dependecies
      }
    }
    module.exports = Parser
    
    • 调用 Parser.getDependecy 方法,获取主模块的依赖路径,修改源码。
    • getDependecy(): 静态方法,是对 type 为 ImportDeclaration 的节点的处理。
    • node.source.value: 就是 import 的值。
    • 因为我们打包后的代码,入参部分的 key 变成了 ./src/data.js,所以这里也需要做出相应的改变

    import data from './data.js' ==> require('./data.js') ==> require('./src/data.js')

    relativepath: 这里获取的是依赖的文件路径
    dependecies: 是收集的依赖对象,key 为 node.source.value ,value 为转换后的路径。

    import data from './data.js'
    import random from './random.js'
    

    node.source.value: 指的是 from 后面的 './data.js' 、'./random.js'

    path.relative(from, to): 方法根据当前工作目录返回 ( from ) 到 ( to ) 的 ( 相对路径 )

    process.cwd(): 返回 Node.js 进程的当前工作目录(path.resolve())

    // selfpack/compilation.js
    const Parser = require('./Parser')
    const path = require('path')
    
    class Compilation {
      constructor(compiler) {
        const { options } = compiler
        this.options = options
        this.entryId
        // 增加
        this.root = process.cwd() // 执行命令的当前目录
      }
      buildModule(absolutePath, isEntry) {
        let ast = ''
        ast = Parser.ast(absolutePath)
        const relativePath = './' + path.relative(this.root, absolutePath)
        if(isEntry){
          this.entryId = relativePath
        }
        const dependecies = Parser.getDependecy(ast, relativePath)
        console.log("依赖项", dependecies)
      }
    }
    module.exports = Compilation
    

    遍历之后 在 ast 里面找到节点类型, 通过 index.js 的 ast 获取到 index.js 文件的依赖(也就是data.js、random.js)

    手写一个简单的 webpack 编译代码

    主模块的依赖路径已经全部找到啦! 走到这一步,离成功就不远了。

    4.4 转换代码

    接下来是转换代码,就是将修改后的 AST 转换成 JS 代码。
    用到了 @babel/core 的 transformFromAst 和 @babel/preset-env。
    安装一下 npm install @babel/core @babel/preset-env

    • transformFromAst: 就是将我们传入的 AST 转化成我们在第三个参数(@babel/preset-env)里配置的模块类型,会返回转换后的代码

    @babel/preset-env 是将我们使用的 JS 新特性转换成兼容的代码。

    此时 Parser.js 长这样

    // selfpack/Parser.js 完整
    const traverse  = require('@babel-traverse').default
    const fs = require('fs')
    const parser = require('@babel/parser')
    const path = require('path')
    // 增加
    const { transformFromAst } = require('@babel/core')
    
    class Parser{
      static ast(path){
        const content = fs.readFileSync(path, 'utf-8') // 读取文件
        const _ast = parser.parse(content, {
          sourceType: 'module' //表示我们要解析的是ES模块
        })
        console.log(_ast)
        console.log('我是body内容', _ast.program.body)
        return _ast
      }
      static getDependecy(ast, file) {
        const dependecies = {}
        traverse(ast, {
          ImportDeclaration: ({node}) => {
            const oldValue = node.source.value
            const dirname = path.dirname(file)
            const relativepath = "./" + path.join(dirname, oldValue) 
            dependecies[oldValue] = relativepath
            node.source.value = relativepath // 将 ./data.js 转化成 ./src/data.js
          }
        })
        return dependecies
      }
      // 增加
      static transform(ast) {
        const { code } = transformFromAst(ast, null, {
            presets: ['@babel/preset-env']
        })
        return code
      }
    }
    module.exports = Parser
    
    // selfpack/compilation.js
      ...
      buildModule(absolutePath, isEntry) {
        let ast = ''
        ast = Parser.ast(absolutePath)
        const relativePath = './' + path.relative(this.root, absolutePath)
        if(isEntry){
          this.entryId = relativePath  // 保存主入口的文件路径
        }
        const dependecies = Parser.getDependecy(ast, relativePath)
        // 增加
        const transformCode = Parser.transform(ast)
        console.log("转换后的代码 ", transformCode)
        return {
          relativePath,
          dependecies,
          transformCode 
        }
      }
    }
      ...
    

    先来看下结果:

    手写一个简单的 webpack 编译代码

    可以看到 const 成功转换成了 var,但是 require("./data.js") 引用的路径还没有和 modules 的 key 保持一致。

    4.5 递归收集依赖

    我们怎么去确定一个模块应该包含什么信息呢?
    首先要确定这个文件的唯一性,所以我们需要要的文件路径,因为这个是唯一的。
    然后再来分析文件的内容:

    • 是否引入了其他文件
    • 自己的主体内容

    所以我们需要的模块信息如下:

    • 该模块的路径
    • 该模块的依赖
    • 该模块转换后的代码

    这里我们获取转换后的代码,并在 buildModule 返回一个对象,返回值结构如下:

    // 获取的模块信息
      {
        relativePath: './src/xxx',
        dependecies: {
          './data.js': './src/data.js',
          './random.js': './src/random.js'
        },
        transformCode: {
          ...
        }
      }
    

    但是 buildModule 只能收集一个模块的依赖,而我们最终的目的是收集所有依赖,所以我们要做一个递归处理。 修改一下 compiler.js

    // selfpack/compiler.js
      ...
      compile() {
        const compilation = new Compilation(this)
         //通过entry找入口文件
        const entryModule = compilation.buildModule(this.options.entry, true)
    
        //  增加
        this.modules.push(entryModule)
        this.modules.map((_module) => {
          const deps = _module.dependecies
          for (const key in deps){
            if (deps.hasOwnProperty(key)){
              this.modules.push(compilation.buildModule(deps[key], false))
            }
          }
        })
        console.log('最终的 modules', this.modules)
      }
      ...
    

    先来看一下 compile 中递归的方法:

    1. 将主入口文件传入buildModule ,得到主入口的文件模块
    2. 最外层遍历的主入口文件的模块
    3. 然后获取主模块的依赖所有模块
    4. 把依赖的模块 push 到 this.modules 里

    来看一下最终的 modules

    手写一个简单的 webpack 编译代码

    成功得到了包含所有模块的:路径、依赖、转换后的代码。

    五、生成 webpack 模版文件

    编译的最后一步就是 生成模板文件,并放到 output 目录。
    我们直接借用文章开头那段打包出来的 dist/main.js 文件的内容,然后做些修改。
    来看修改后的的 compilation.js

    // selfpack/compilation.js 完整
    const path = require('path')
    const Parser = require('./Parser')
    const fs = require('fs')
    
    
    class Compilation {
      constructor(compiler) {
        // 修改
        const { options, modules } = compiler
        this.options = options
        this.root = process.cwd() // 执行命令的当前目录
        this.entryId
        // 增加
        this.modules = modules
      }
      buildModule(absolutePath, isEntry) {
        let ast = ''
        ast = Parser.ast(absolutePath)
        const relativePath = './' + path.relative(this.root, absolutePath)
        if(isEntry){
          this.entryId = relativePath
        }
        const dependecies = Parser.getDependecy(ast, relativePath)
        const transformCode = Parser.transform(ast)
        // console.log("依赖项", dependecies)
        // console.log("转换后的代码 ", transformCode)
        return {
          relativePath,
          dependecies,
          transformCode 
        }
      }
      // 增加
      emitFiles(){
        let _modules = ''
        const outputPath = path.join(
          this.options.output.path,
          this.options.output.filename
        )
        this.modules.map((_module) => {
          // 记得加引号
          _modules += `'${_module.relativePath}': function(module, exports, require){
            ${_module.transformCode}
          },`
        })
        const template = `
        (function(modules) {
          var installedModules = {};
          function __webpack_require__(moduleId) {
            // Check if module is in cache
            if(installedModules[moduleId]) {
              return installedModules[moduleId].exports;
            }
            var module = installedModules[moduleId] = {
              exports: {}
            };
            modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
            
            return module.exports;
          }
           // 执行的入口函数
          return __webpack_require__('${this.entryId}');
        })({
          ${_modules}
        })
        `
        const dist = path.dirname(outputPath)
        fs.mkdirSync(dist)
        fs.writeFileSync(outputPath, template, 'utf-8')
      }
    }
    module.exports = Compilation
    

    打包之后的文件内容,大体上长这样,还有点小瑕疵。
    看下 emitFiles 函数的作用

    1. 获取 selfpack.config.js 中的 output 对象的 path,filename
    2. 遍历所有的 modules 并放在模板的入参位置
    3. 新建一个文件,将编译后的代码写入

    完整的 compiler

    // selfpack/compiler.js 完整
    const { SyncHook } = require('tapable')
    const Compilation = require('./Compilation')
    
    class Compiler {
      constructor(options) {
        this.modules = []
        this.options = options
        this.hooks = {
          run: new SyncHook()
        }
      }
      run() {
        this.compile()
      }
      compile() {
        const compilation = new Compilation(this)
        const entryModule = compilation.buildModule(this.options.entry, true)
        this.modules.push(entryModule)
        this.modules.map((_module) => {
          const deps = _module.dependecies
          for (const key in deps){
            if (deps.hasOwnProperty(key)){
              this.modules.push(compilation.buildModule(deps[key], false))
            }
          }
        })
        // 增加
        compilation.emitFiles()
      }
    }
    module.exports = Compiler
    

    编译后的代码如下:

    // dist/main.js
    (function (modules) {
      var installedModules = {};
      function __webpack_require__(moduleId) {
        // Check if module is in cache
        if (installedModules[moduleId]) {
          return installedModules[moduleId].exports;
        }
        var module = installedModules[moduleId] = {
          exports: {}
        };
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        return module.exports;
      }
      // 执行的入口函数
      return __webpack_require__('./src/index.js');
    })({
      './src/index.js': function (module, exports, require) {
        "use strict";
    
        var _data = _interopRequireDefault(require("./src/data.js"));
    
        var _random = _interopRequireDefault(require("./src/random.js"));
    
        function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
    
        console.log('?我是数据文件--->', _data["default"]);
        console.log('?我是随机数--->', _random["default"]);
        console.log('?我是index.js');
      }, './src/data.js': function (module, exports, require) {
        "use strict";
    
        Object.defineProperty(exports, "__esModule", {
          value: true
        });
        exports["default"] = void 0;
        var result = '我是文件里面的数据';
        var _default = result;
        exports["default"] = _default;
      }, './src/random.js': function (module, exports, require) {
        "use strict";
    
        Object.defineProperty(exports, "__esModule", {
          value: true
        });
        exports["default"] = void 0;
        var random = Math.random();
        var _default = random;
        exports["default"] = _default;
      },
    })
    

    走到这里一个简单 webpack 的编译流程代码就算写完啦。
    把代码复制到浏览器测试一下

    六、实现 webpack 的 Plugins 功能

    怎么开发一个自定义的plugins?
    webpack中内部实现了自己的一套生命周期,而 plugins 就是用 apply 来调用webpack里面提供的生命周期。
    而 webpack 的生命周期主要就是 tapable 来实现的。
    这里只用到了 SyncHook,更多可参考这篇 Tapable 详解。

    我们修改一下官网的 ConsoleLogOnBuildWebpackPlugin.js 例子。
    在 src同级目录新建一个 plugins

      + src
      - plugins
        - ConsoleLogOnBuildWebpackPlugin.js
    

    编写一个简单的 plugins

    // ConsoleLogOnBuildWebpackPlugin.js
    const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
    
    class ConsoleLogOnBuildWebpackPlugin {
      apply(compiler) {
        compiler.hooks.run.tap(pluginName, compilation => {
          console.log('The webpack build process is starting!!!');
        });
        // 在文件打包结束后执行
        compiler.hooks.done.tap(pluginName,(compilation)=> {
          console.log("整个webpack打包结束")
        })
        // 在webpack输出文件的时候执行
        compiler.hooks.emit.tap(pluginName,(compilation)=> {
            console.log("文件开始发射")
        })
      }
    }
    module.exports = ConsoleLogOnBuildWebpackPlugin;
    

    然后再配置文件引入这个 plugins

    // selfpack.config.js
    const { join } = require('path')
    const ConsoleLogOnBuildWebpackPlugin = require('./plugins/ConsoleLogOnBuildWebpackPlugin')
    
    module.exports = {
      entry: join(__dirname, './src/index.js'),
      output: {
        path: join(__dirname, './dist'),
        filename: 'main.js'
      },
      plugins: [new ConsoleLogOnBuildWebpackPlugin()],
    }
    

    要让我们的 selfwebpack 支持 plugins ,还要做些改动。

    // selfpack/index.js
    const Compiler = require('./Compiler')
    const options = require('../selfpack.config.js')
    const compiler = new Compiler(options)
    const plugins = options.plugins
    for (let plugin of plugins) {
        plugin.apply(compiler)
    }
    compiler.run()
    
    // selfpack/compiler.js
    const { SyncHook } = require('tapable')
    const Compilation = require('./Compilation')
    
    class Compiler {
      constructor(options) {
        this.modules = []
        this.options = options
        this.hooks = {
          run: new SyncHook(),
          // 增加
          emit: new SyncHook(),
          done: new SyncHook()
        }
      }
      run() {
        this.compile()
      }
      compile() {
        const compilation = new Compilation(this)
        // 增加
        this.hooks.run.call()
         //通过entry找入口文件
        const entryModule = compilation.buildModule(this.options.entry, true)
        this.modules.push(entryModule)
        this.modules.map((_module) => {
          const deps = _module.dependecies
          for (const key in deps){
            if (deps.hasOwnProperty(key)){
              this.modules.push(compilation.buildModule(deps[key], false))
            }
          }
        })
        // console.log('最终的 modules', this.modules)
        compilation.emitFiles()
        // 增加
        this.hooks.emit.call()
        this.hooks.done.call()
    
      }
    }
    module.exports = Compiler
    

    在 compiler 函数一初始化的时候就定义自己的 webpack 的生命周期,并且在 run 期间进行相应的调用,这样我们就实现了自己的生命周期。

    打印结果如下:
    手写一个简单的 webpack 编译代码

    本文只实现了简单的编译原理,更多实现请看 webapck-github

    对应的代码放到了这里 github

    参考文章:手写webpack核心原理


    下载网 » 手写一个简单的 webpack 编译代码

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元