动手实现一个简易的webpack

781 阅读3分钟

往期文章:

todolist

  • 将ES6转换成ES5语法
    • 通过babylon生成AST
    • 通过babel-core将AST重新生成源码
  • 分析模块之间的依赖关系
    • 通过 babel-traverse 的 ImportDeclaration方法获取依赖属性
  • 生成js文件可以在浏览器中运行

基础准备

给自定义webpack 起个名字 x-webpack

安装基础插件

npm install --dev-save @babel/core @babel/preset-env @babel/traverse babylon magic-string

package.json

{
  "name": "x-webpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "bin": {
    "x-webpack": "./lib/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

本地安装软链

  • x-webpack目录下,执行 npm link

指定lib/index.js 文件类型(用于bin的执行)

  • #!/usr/bin/env node

x-webpack 开整

  • index.js,执行文件
const { resolve } = require('path')
const Compiler = require('./compiler')
const defaultConfig = require('./default_config') // 默认配置
const customConfig = require(resolve('./x.config.js')) // 加载个人项目配置,这里约定配置文件为 x.config.js

const config = { ...defaultConfig, ... customConfig }

// 开启 xwebpack 打包
new Compiler(config).run()
  • default_config.js 默认配置
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'boundle.js'
  }
}
  • compiler.js 编译器
const path = require('path')
const fs = require('fs')
const { getAST, getDependencis, transform } = require('./parser')

module.exports = class Compiler{
  constructor(options) {
    const { entry, output } = options
    this.entry = entry
    this.output = output
    this.modules = [] // 收集的所有模块
    this.root = process.cwd() // 就是x-webpack-demo目录
  }
  run() {
    // 搜集依赖
    const entryModule = this.buildModule(this.entry, true)
    this.modules.push(entryModule)
    this.modules.map((_module) => {
      _module.dependencies.map((dependency) => {
        this.modules.push(this.buildModule(dependency))
      })
    })
    // 生成文件
    this.emit()
  }
  buildModule(filename, isEntry) {
    let ast;
    if (isEntry) {
      ast = getAST(filename)
    } else {
      // process.cwd() 表示根目录
      let absolutePath = path.join(this.root, './src', filename)
      ast = getAST(absolutePath)
    }
    return {
      filename,
      dependencies: getDependencis(ast),
      transformCode: transform(ast)
    }
  }
  emit() {
    const outputPath = path.join(this.output.path, this.output.filename);
    let modules = ''
    this.modules.map((_module) => {
      modules += `'${ _module.filename }': function (require, module, exports) { ${ _module.transformCode } },`
    });
    const bundle = `
      (function(modules) {
        function require(fileName) {
            const fn = modules[fileName];
            const module = { exports : {} };
            fn(require, module, module.exports);
            return module.exports;
        }
        require('${this.entry}');
      })({${modules}})
    `
    try {
      // 检查目录是否存在, 没有则创建
      if(!fs.existsSync(this.output.path)) {
        fs.mkdirSync(this.output.path)
      }
  
      // 输出
      fs.writeFileSync(outputPath, bundle, 'utf-8')
    } catch (e) {
      throw new Error(e)
    }
  }
}
  • parse.js 解析babel
const fs = require('fs')
const babylone = require('babylon')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')
// const MagicString = require('magic-string')

module.exports = {
  // 生成AST
  getAST: filename => {
    // 读取入口文件内容
    const content = fs.readFileSync(filename, 'utf-8')
    // 生成AST语法分析树
    return babylone.parse(content, {
      sourceType: 'module'
    })
  },
  // 分析依赖
  getDependencis: ast => {
    const dependencies = []
    // 遍历AST树
    traverse(ast, {
      ImportDeclaration({ node }) {
        dependencies.push(node.source.value);
        // 魔法字符串处理
        // const code = new MagicString(content)
        // const { start, end, specifiers , source } = node
        // 替换内容
        // code.overwrite(start, end, `var ${specifiers[0].local.name} = __webpack_require__("${source.value}")`)
      }
    })
    return dependencies
  },
  transform: ast => {
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/env']
    })
    return code
  }
}

使用x-webpack

  • 创建 x-webpack-demo

  • package.json

{
  "name": "x-webpack-demo",
  "version": "1.0.0",
  "description": "",
  "main": "x.config.js",
  "scripts": {
    "build": "x-webpack" // x-webpack已经通过npm link 软链到本地了
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
  • x.config.js
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js'
  }
}
  • src/index.js
const sayHi = require('./a.js')
sayHi('xfz')
  • src/a.js
const getYear = require('./common/util.js')
module.exports = (name) => {
  console.log(`hello:${name}`)
  getYear()
}
  • src/common/util.js
module.exports = age => {
  console.log(`I am ${age} years old`)
}
  • index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="./main.js"></script>
</body>
</html>

他来啦,他来啦,他带着绿帽走来啦...

打包:npm run build

####打包产出物

(function(modules){
  const installedModules = {}
  function __webpack_require__(id) {
    // 如果 installedModules 中有就直接获取
    if (installedModules[id]) {
      return installedModules[id].exports
    }
    let module = installedModules[id] = {
      exports: {}
    }
    // 没有的话从 modules 中获取 function 然后执⾏,
    modules[id].call(modules.exports, module, module.exports, __webpack_require__)
    return module.exports
  }
  // 入口和modules使用占位符号
  return __webpack_require__("./src/index.js")
})("./src/index.js":function(module, exports, __webpack_require__){
      eval('const sayHi = __webpack_require__("./src/a.js")
sayHi('xfz')')
    }
    ,"./src/a.js":function(module, exports, __webpack_require__){
      eval('const getYear = __webpack_require__("./src/common/util.js")
module.exports = (name) => {
  console.log(`hello:${name}`)
  getYear()
}')
    }
    ,"./src/common/util.js":function(module, exports, __webpack_require__){
      eval('module.exports = age => {
  console.log(`I am ${age} years old`)
}')
    }
    )

结尾

  • 整出来来,是不是感觉自己萌萌哒....

❤️ 加入我们

字节跳动 · 幸福里团队

Nice Leader:高级技术专家、掘金知名专栏作者、Flutter中文网社区创办者、Flutter中文社区开源项目发起人、Github社区知名开发者,是dio、fly、dsBridge等多个知名开源项目作者

期待您的加入,一起用技术改变生活!!!

招聘链接: job.toutiao.com/s/JHjRX8B