webpack原理篇

65 阅读3分钟

一、 webpack编译流程

  • Webpack CLI启动打包流程
  • 载入Webpack核心模块,创建Compiler对象
  • 使用Compiler对象开始编译整个项目
  • 从入口文件开始,解析模块依赖,形成依赖关系树
  • 递归依赖树,将每个模块交给对应的Loader处理
  • 合并Loader处理完的结果,将打包结果输出到dist目录

二、 webpack代码实现

1. Babel的编译原理

1. 解析
  • 接收代码并输出 AST。 这个步骤分为两个阶段:词法分析(Lexical Analysis) 和 语法分析(Syntactic Analysis)。
  • 词法分析阶段把字符串形式的代码转换为 令牌(tokens) 流。
  • 语法分析阶段会把一个令牌流转换成 AST 的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操作。

Babel 使用 @babel/parser 解析代码,输入的 js 代码字符串根据 ESTree 规范生成 AST(抽象语法树)

code(字符串形式代码) -> tokens(令牌流) -> AST(抽象语法树)
2. 转换
  • 接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。
  • Babel提供了@babel/traverse(遍历)方法维护这AST树的整体状态,并且可完成对其的替换,删除或者增加节点,这个方法的参数为原始AST和自定义的转换规则,返回结果为转换后的AST。
3. 生成
  • 把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。
  • Babel使用 @babel/generator 将修改后的 AST 转换成代码,生成过程可以对是否压缩以及是否删除注释等进行配置,并且支持 sourceMap。

2. bundle.js简易实现

  • 依赖收集
  • ES6转ES5
  • 替换require与exports
(function(list) {
  function require(file) {
    var exports = {}
    (function(exports, code) {
      eval(code)
    })(exports, list[file])
    
    return exports
  }
  
  require("index.js")
})({
  "index.js": `
    var add = require('add.js'),default
    console.log(add(1, 2))
  `,
  "add.js": `
    exports.default = function(a, b) { return a + b }
  `
})

3. 基于babel手写webpack

1. 安装依赖
yarn add @babel/core @babel/parser @babel/traverse @babel/preset-env
2. 代码实现
// webpack.ts
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse')
const babel = require('@babel/core')
​
/*
* 分析单独模块
*/
function getModuleInfo(file) {
  // 读取文件
  const body = fs.readFileSync(file, 'utf-8')
  // 转换AST
  // 代码字符串 => 对象 => 对象遍历解析
  const ast = parser.parse(body, {
    sourceType: 'module'
  })
  const deps = {}
  
  traverse(ast, {
    //visitor
    ImportDeclaration({ node }) {
      // 遇到import节点回调
      const dirname = path.dirname(file)
      const abspath = './' + path.join(dirname, node.source.value)
      deps[node.source.value] = abspath
    }
  })
  
  // ES6转ES5
  const { code } = babel.transformFromAst(ast, null, {
    presets: ['@babel/preset-env']
  })
  
  return {
    file,
    deps,
    code
  }
}
​
/*
* 解析模块
*/
function parseModules(file) {
  const entry = getModuleInfo(file)
  const temp = [entry]
  const depsGraph = {}
  
  getDeps(temp, entry)
  
  // 构建依赖树
  temp.forEach(info => {
    depsGraph[info.file] = {
      deps: info.deps,
      code: info.code
    }
  })
  
  return depsGraph
}
​
/*
* 获取依赖
*/
function getDeps(temp, { deps }) {
  Object.keys(deps).forEach(key => {
    const child = getModuleInfo(deps[key])
    temp.push(child)
    getDeps(temp, child)
  })
}
​
/*
* 生成bundle
*/
function bundle(file) {
  const depsGraph = JSON.stringify(parseModules(file))
  
  return `(function(graph) {
    function require(file) {
      function absRequire(relPath) {
        return require(graph[file].deps[relPath])
      }
      
      var exports = {}
      (function(require, exports, code) {
        eval(code)
      })(absRequire.exports, graph[file].code)
      
      return exports
    }
    require('${file}')
  })(${depsGraph})`
}
​
const content = parseModules('./src/index.js')
// console.log('content:', content)
!fs.existsSync("./dist") && fs.mkdirSync("./dist")
fs.writeFileSync("./dist/bundle.js", content)

4. webpack的devServer/proxy

webpack的proxy是基于node插件http-proxy-middleware,http-proxy-middleware为何能解决跨域问题:

  • 首先,浏览器报跨域,并不是服务器没有返回,而是,服务器返回了,但是被浏览器拦截了。
  • 其次,http-proxy-middleware解决跨域的方案,本地起了一个node代理服务器(var httpProxy = require('http-proxy')),通过代理服务器去请求目标服务器,然后返回请求结果。由于浏览器请求的是本地路径,所以不会有跨域问题。