Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

实现一个mini-webpack #32

Open
YIngChenIt opened this issue Jul 1, 2020 · 0 comments
Open

实现一个mini-webpack #32

YIngChenIt opened this issue Jul 1, 2020 · 0 comments

Comments

@YIngChenIt
Copy link
Owner

实现一个mini-webpack

本来记录自己实现一个mini-webpack时候遇到的坑和总结webpack的大致流程

初始化

我们需要写一个webpack的包,首先需要初始化一下项目

npm init -y

然后我们配置一下package.jsonbin选项

// package.json
"bin": {
    "cy-pack": "./bin/cy-pack.js"
},

然后我们需要新建一个bin文件夹和新起一个cy-pack.js文件,我们看下文件的内容

#! /usr/bin/env node

// 拿到当前执行名的路径 拿到webpack.config.js
const path = require('path')

const config = require(path.resolve(process.cwd(), 'webpack.config.js'))

const Compiler = require('./lib/Compiler.js')

const compiler = new Compiler(config)

compiler.run()

#! /usr/bin/env node表示当前运行环境是node环境,下面就是对应的代码,这里不做解释了

最后通过命令npm link将这个包映射到全局npm上去,在另外一个文件夹使用我们的包的时候只需要npm link cy-pack就好啦

手写一个最简单的打包工具

// Compiler.js
const path = require('path')
const fs = require('fs')
const babylon = require('babylon') //将源码转化为AST
const traverse = require('@babel/traverse').default
const t = require('@babel/types')
const generator = require('@babel/generator').default
const ejs = require('ejs')


class Compiler {
    constructor (config) {
        this.config = config
        this.entryId //入口文件的路径
        this.modules = {} // 所有依赖的模块
        this.entry = config.entry // 入口路径
        this.root = process.cwd() //当前工作路径
    }
    getSource (modulePath) {
        return fs.readFileSync(modulePath, 'utf8')
    }
    parse (source, parentPath) { // 靠AST解析语法树解析源码
       const ast =  babylon.parse(source)
       const dependencies = [] //依赖的数组
       traverse(ast, {
           CallExpression(p) {
               let node = p.node
               if (node.callee.name  === 'require') { //找到require
                 node.callee.name = '__webpack_require__'
                 let moduleName = node.arguments[0].value //取到require引入的名字
                 moduleName = moduleName + (path.extname(moduleName) ? '' : '.js') //如果没有后缀就补充.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) { // 构建模块
        const source = this.getSource(modulePath) //拿到模块的内容
        const moduleName = './' + path.relative(this.root, modulePath)
        if (isEntry) { //如果是根模块
            this.entryId = moduleName
        }
        const { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))
        dependencies.forEach(dep => { //递归加载
            this.buildModule(path.join(this.root, dep), false)
        })
        this.modules[moduleName] = sourceCode
    }
    emitFile () { //发射打包后的文件
        // 用数据渲染我们的模板 然后输出到对应的文件去
        const main = path.join(this.config.output.path, this.config.output.filename)
        const templateStr = this.getSource(path.join(__dirname, 'main.ejs'))
        const code = ejs.render(templateStr, { //将数据传入ejs 然后得到代码块
            entryId: this.entryId,
            modules: this.modules
        })
        this.assets = {}
        this.assets[main] = code
        fs.writeFileSync(main, this.assets[main])
    }
    run () {
        // 执行并创建模块的依赖关系
        this.buildModule(path.resolve(this.root, this.entry), true)
        this.emitFile()
    }
}

module.exports = Compiler

我们来总结一下基本的流程

  • 通过在使用我们包的目录使用npx cy-pack运行我们的打包工具,然后我们可以拿到当前执行这个命令的路径

  • 根据这个路径我们可以拿到webpack.config.js,并将对应的数据进行存储

  • 然后调用buildModule创建我们的模块关系,主要思路是通过配置中的入口路径读取到入口文件的内容,然后调用bodylon.parse将读到的内容转化为AST, 然后通过traverse找到代码中的require,将他替换成我们自己实现的__webpack_require__,然后替换对应require中的路径,最后生成依赖表单就好了(依赖表单是处理过的路径)

  • 然后通过遍历依赖表单调用buildModule方法递归处理完全部依赖的文件

  • 最后可以通过ejs模板等方式将数据写入模板中,最后输出到配置的output对应的路径和文件就好了

我们来看下最基本的模板

// main.ejs
(function (modules) { // webpackBootstrap
    // The module cache
    var installedModules = {};
    // The require function
    function __webpack_require__(moduleId) {
      // Check if module is in cache
      if (installedModules[moduleId]) {
        return installedModules[moduleId].exports;
      }
      // Create a new module (and put it into the cache)
      var module = installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
      };
      // Execute the module function
      modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
      // Flag the module as loaded
      module.l = true;
      // Return the exports of the module
      return module.exports;
    }
    return __webpack_require__(__webpack_require__.s = "<%-entryId%>");
  })
    ({
        <%for(let key in modules){%>
            "<%-key%>":
            (function (module, exports, __webpack_require__) {
            eval(`<%-modules[key]%>`);
            }),
        <%}%>  
    });

支持loader

支持plugin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant