# Webpack 原理

# 准备

  1. 新建 bin 目录,将打包工具主程序放入其中 主程序的顶部当有:#!/usr/bin/env node标识,指定程序的执行环境为 node
#!/usr/bin/env node

console.log('测试')
  1. 在 package.json 中配置 bin 脚本
"bin": {
  "pack": "./bin/pack.js"
}
  1. 通过 npm link 链接到全局包中,供本地测试使用

# webpack_require函数

# 输出 bundle.js 过程分析

  1. 配置简单的 entry 和 output 参数
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  mode: 'development'
}
  1. 读取 index.js 文件的内容,通过@babel/parser进行语法分析,输出 AST(抽象语法树); 通过@babel/traverse遍历操作 AST,找到p.node.callee.name === 'require'的节点, 修改 AST 节点属性 p.node.callee.name = '__webpack_require__', 修改路径,并避免 windows 出现反斜杠:\ p.node.arguments[0].value, 通过@babel/generator把 AST 映射回 JS 代码。
depAnalyse(modulePath) {
  // 读取index.js文件的内容
  let source = this.getSource(modulePath)
  let ast = parser.parse(source)
  // 存储当前模块的所有依赖
  let dependencies = []
  traverse(ast, {
    CallExpression(p) {
      if (p.node.callee.name === 'require') {
        // 修改require
        p.node.callee.name = '__webpack_require__'
        // 修改路径,并避免windows出现反斜杠:\
        let oldVal = p.node.arguments[0].value
        oldVal = `./${path.join('src', oldVal)}`
        p.node.arguments[0].value = oldVal.replace(/\\+/g, '/')
        dependencies.push(p.node.arguments[0].value)
      }
    }
  })
  // 解析新的ast
  let sourceCode = generator(ast).code
  // 构建modules对象
  let modulePathRelative = `./${path.relative(this.root, modulePath)}`.replace(/\\+/g, '/')
  this.modules[modulePathRelative] = sourceCode
  // 递归加载所有依赖
  dependencies.forEach(item => {
    this.depAnalyse(path.resolve(this.root, item))
  })
}
  1. 使用 ejs 语法编写 output.ejs 的文件,渲染 modules 对象并生成类似使用 webpack 构建输出的 bundle.js 文件。
emitFile() {
  let template = this.getSource(path.join(__dirname, '../template/output.ejs'))
  let reuslt = ejs.render(template, {
    entry: this.entry,
    modules: this.modules
  })
  let output = this.config.output
  let outputPath = path.join(output.path, output.filename)
  fs.writeFileSync(outputPath, reuslt)
}

# loader

加载顺序:pre 前置,post 后置,遵循:pre > inline > normal > post (前置 > 行内 > 普通 > 后置)

{
  test: /\.js$/,
  use: './loaders/loader.js',
  enforce: 'pre'
}

# tapable 简介

在 webpack 内部实现事件流机制的核心在于tapable,有了它就可以通过事件流的形式, 将各个插件串联起来,tapable 类似于 node 中的 events 库,核心原理也是发布订阅模式

# 在 itheima-pack 中添加 loader 的功能

通过配置 loader 和手写 loader 可以发现,其实 webpack 能支持 loader,主要步骤如下:

  1. 读取 webpack.config.js 配置文件的 module.rules 配置项,进行倒序迭代(rules 的每项匹配规则按倒序匹配)
  2. 根据正则匹配到对应的文件类型,同时再批量导入 loader 函数
  3. 倒序迭代调用所有 loader 函数(loader 的加载顺序从右到左,也是倒叙)
  4. 最后返回处理后的代码

在实现 itheima-pack 的 loader 功能时,同样也可以在加载每个模块时,根据 rules 的正则来匹配是否满足条件,如果满足条件则加载对应的 loader 函数并迭代调用

depAnalyse()方法中获取到源码后,读取 loader:

handleModule(modulePath, source) {
  // 读取rules规则,倒序遍历
  let rules = this.config.module.rules
  let len = rules.length - 1
  const handleLoader = (use, obj) => {
    let loaderPath = path.join(this.root, use)
    let loader = require(loaderPath)
    return loader.call(obj, source)
  }
  for (let i = len; i >= 0; i--) {
    const {use, test} = rules[i]
    // 获取每条规则,与当前modulePath进行匹配
    if (test.test(modulePath)) {
      if (Array.isArray(use)) {
        for (let j = use.length - 1; j >= 0; j--) {
          source = handleLoader(use[j])
        }
      }
      else if (typeof use === 'string') {
        source = handleLoader(use)
      }
      else if (use instanceof Object) {
        source = handleLoader(use.loader, { query: use.options })
      }
    }
  }
  // console.log(source)
  return source
}

# webpack 插件的组成 (opens new window)

  • 一个 JavaScript 命名函数
  • 在插件函数的 prototype 上定义一个 apply 方法
  • 指定一个绑定到 webpack 自身的事件钩子
  • 处理 webpack 内部实例的特定数据
  • 功能完成后调用 webpack 提供的回调

# plugin 生命周期钩子函数

# Compiler 和 Compilation 的区别

  • compiler 对象表示不变的 webpack 环境,是针对 webpack 的
  • compilation 对象针对的是随时可变的项目文件,只要文件有改动,compilation 就会被重新创建

# cheerio

cheerio 是 nodejs 的抓取页面模块,为服务器特别定制的,快速、灵活、实施的 jQuery 核心实现。 适合各种 Web 爬虫程序

# 项目地址 (opens new window)