2.概念入口—入—上-灵析社区

懒人学前端

一、资源处理流程

资源处理流程

借用 webpack 官网对 webpack 的描述: webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle

什么是bundle

代码分离是 webpack 的特性之一,使用 entry 配置入口起点,会将代码分成源代码和分发代码

其中源代码是开发编辑的代码,分发代码是经过 webpack 构建,可能经过转义、压缩或优化后的代码,这些代码存放于 bundle 中,可以被浏览器等环境直接运行

什么是dependency graph

看了上面的内容,其实我们还是不清楚 webpack 到底做了哪些事情使浏览器不支持的语法变得可以执行,而去查看源码,会发现源码中代码对我们不是特别友好,经常左跳右跳无法持续跟踪,所以针对 webpack 打包流程,总结了下面的一个简版打包代码,让我们能大体看清 webpack 做了哪些操作,以下只是简版 demo,没有强行靠近 webpack 打包结果,让我们更能清晰的梳理流程。

打包步骤总结

  1. 我们工程的依赖路径 index.js -> hello.js -> message.js
  2. 根据 webpack.config.js 中定义的 entry 解析入口(index.js)文件,找到他的依赖
  3. 递归的构建依赖关系图
  4. 将所有内容打包到 webpack.config.js 定义的 output 文件

在整理打包内容之前,我们先来看下我们现在项目的结构,项目名称为 webpack_demo,其中包含 webpack.config.js,package.json,dist/main.js,src/index.js,src/hello.js,src/message.js,src/bundler.js,src/complier.js。

webpack.config.js 定义对象,导出项目入口出口文件

const path = require('path')
module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'main.js'
    }
}

src/index.js 代码中导入 hello.js方法并执行

import hello from './hello.js'
console.log(hello())

src/hello.js 代码中导入 message.js中的message参数

import { message } from './message.js'
export default function() {
    return 'hello ' + message;
}

src/message.js 中定义了一个message变量

export const message = 'world!!!'

上面的代码层层引用下来,可以在控制台输出 ‘hello world!!!’,就代表打包成功了。

上面的环境已经定义完成,接下来让我们按照步骤完成打包操作:

1.在src/complier.js文件中创建complier构造函数,在构造函数中获取webpack.config.js中定义的入口出口参数

module.exports = class Complier {
    constructor(options) {
      const { entry, output } = options
      this.entry = entry
      console.log(options)
      this.output = output
    }
}

2.在 src/bundler.js 文件中引入 webpack.config.js 文件,创建 Complier 的实例化对象并传入 options

const complier = require('./complier')
const options = require('../webpack.config')
new complier(options)

在命令行执行 node src/bundler.js 后,在控制台会打印出 options 内容

3.拿到配置参数后,开始根据入口文件解析文件内容,解析单流程整体为:

  1. 根据入口文件名称通过 nodejs 提供的方法 fs.readFileSync 读取到文件内容
  2. 使用 @babel/parser 将文件内容转换成ast语法树
  3. ast 语法树中 node 节点中包含了文件依赖的文件名称,使用 @babel/traverse 方法提取出依赖的文件名称并存储到一个数组中
  4. 通过 @babel/core 中的 babel.transformFromAst 方法将 ast 转换成目标浏览器可执行的代码
  5. 将上述获取的参数返回个对象,对象包含文件名,依赖数组,文件可执行代码,这个对象即为一个依赖图谱中的一个节点
  6. 遍历入口文件的依赖数组,由于数组中是文件名,则递归执行上述方法,直到找到所有依赖
  7. 返回所有依赖对象

根据上面总结内容我们在 src/complier.js 中创建一个 createAsset 方法

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
...
// 开始编译,构建ast语法树 filename: ./src/index.js
    createAsset(filename) {
       // 1
       // content 内容即为index.js中书写的内容
      const content = fs.readFileSync(filename, 'utf-8')
      // 2
      // ast 内容为对象,具体内容可以console.log(ast)查看
      // https://astexplorer.net/ 在官网输入index.js内容即可看到对应的树
      const ast = parser.parse(content, {
          sourceType: 'module'
      })
      // 创建依赖对象
      const dependencies = {}
      // 3
      // 获取抽象语法树中的依赖文件名
      traverse(ast, {
            ImportDeclaration: ({node}) => {
             // 获取文件的路径名如 './src/index.js' dirname='./src'
             const dirname = path.dirname(filename)
             const absPath = path.join(dirname, node.source.value)
             // node.source.value: .hello.js
             // absPath: ./src/index.js
             dependencies[node.source.value] = absPath
            }
      })
      // 4
      // 将ast转换成可执行代码 
      // https://www.babeljs.cn/docs/babel-core 将index.js内容直接放在官网即可看到转译后代码
      const { code } = babel.transformFromAst(ast, null, {
        presets: ['@babel/preset-env']
      })
      // 5
      return {
          filename,
          dependencies,
          code
      }
    }

入口文件的依赖关系已经定义好,接下来根据入口文件的 dependencies ,递归遍历出所有子依赖,在 src/complier.js 文件中定义 run 方法

   // 拿到参数、执行、分析入口文件
    run() {
       // 拿到入口文件的依赖
       const mainAsset = this.createAsset(this.entry)
       const queue = [mainAsset]
       // 6
       // 遍历对象
       for (const asset of queue) {
          // 遍历文件的依赖文件,递归创建依赖图
         Object.values(asset.dependencies).forEach(filename => {
             const child = this.createAsset(filename)
             queue.push(child)
         })
       }
       // 7
       return queue
    }

命令行执行 node src/bundler.js 看下 queue 的内容如下

4.依赖树已经拿到,接下来在 src/bundler.js 中获取 complier 中返回的 queue

// 获取dependence graph
const graph = new complier(options).run()

5.在 src/bundler.js 中创建函数 bundle,解析 graph 树,定义 require 函数,定义 modules,通过 eval 函数执行依赖树中的 code,在此我们可以知道 webpack 重写了 require 函数,所以 babel 中转换的函数可以正常执行

function bundle(graph){
  // 得到依赖文件名的对象 
  let modules = {};
  graph.forEach(item => {
     // 将文件名作为key, value为依赖文件,code为文件名对应的函数
    modules[item.filename] = {
        dependencies: item.dependencies,
        code: item.code
    }
  })
  modules = JSON.stringify(modules)
  const result = `(function(graph){
      function require(filepath) {
        function localRequire(relativePath) {
            // 将代码中的require中的路径转换成dependencies存储的带文件夹名的路径
           return require(graph[filepath].dependencies[relativePath])
        }
        var exports = {}
        function fn(require, exports, code) {
            eval(code)
        }
        fn(localRequire, exports, graph[filepath].code)
        return exports
      }
      require('${entry}')
    })(${modules})`
  return result
}

const graph = new complier(options).run()
// 执行bundle函数
const result = bundle(graph)

命令行输出 result 内容,粘贴内容到浏览器控制台并回车执行,发现我们预期的 'hello world!!!' 已经可以正常打印

6.以上我们已经拿到了编译后的代码,最后将它输出到 dist/main.js 中,在 src/bundler.js 中创建方法 createFile(),使用 fs 对象的 writeFileSync 将内容输出,在命令行执行命令后可以看到 src/main.js 中输出了对应内容

function createFile(code) {
    fs.writeFileSync(path.join(output.path, output.filename), code)
}

7.下面是 src/complier.js和 src/bundler.js文件全部内容

complier.js

// 文件操作模块,读取文件内容
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

module.exports = class Complier {
    constructor(options) {
      const { entry, output } = options
      this.entry = entry
      this.output = output
    }
    // 拿到参数、执行、分析入口文件
    run() {
       const mainAsset = this.createAsset(this.entry)
       const queue = [mainAsset]
       for (const asset of queue) {
         Object.values(asset.dependencies).forEach(filename => {
             const child = this.createAsset(filename)
             queue.push(child)
         })
       }
       console.log(queue)
       return queue
    }
    // 开始编译,构建ast语法树 filename: ./src/index.js
    createAsset(filename) {
      const content = fs.readFileSync(filename, 'utf-8')
      const ast = parser.parse(content, {
          sourceType: 'module'
      })
      // 创建依赖
      const dependencies = {}
      traverse(ast, {
            ImportDeclaration: ({node}) => {
             // 获取文件的路径名如 './src/index.js' dirname='./src'
             const dirname = path.dirname(filename)
             const absPath = path.join(dirname, node.source.value)
             dependencies[node.source.value] = absPath
            }
        })
      // 将ast转换成代码 
      // https://www.babeljs.cn/docs/babel-core
      const { code } = babel.transformFromAst(ast, null, {
        presets: ['@babel/preset-env']
      })

      return {
          filename,
          dependencies,
          code
      }
    }
}

bundler.js

// 引入配置
const fs = require('fs');
const path = require('path')
const options = require('../webpack.config')

const complier = require('./complier')

const { entry, output } = options

function bundle(graph){
  // 得到以依赖文件名的对象 
  let modules = {};
  graph.forEach(item => {
    modules[item.filename] = {
        dependencies: item.dependencies,
        code: item.code
    }
  })
  modules = JSON.stringify(modules)
  const result = `(function(graph){
      function require(filepath) {
        function localRequire(relativePath) {
            // 将代码中的require中的路径转换成dependencies存储的带文件夹名的路径
           return require(graph[filepath].dependencies[relativePath])
        }
        var exports = {}
        
        function fn(require, exports, code) {
            eval(code)
        }
        
        fn(localRequire, exports, graph[filepath].code)

        return exports
      }
      require('${entry}')
    })(${modules})`
  return result
}

function createFile(code) {
    fs.writeFileSync(path.join(output.path, output.filename), code)
}

const graph = new complier(options).run()
const result = bundle(graph)
createFile(result)

总结:

通过上面的 demo,我们已经可以大概了解 webpack 的编译流程,当然 webpack 的源码功能强大且复杂,感兴趣的小伙伴儿可以自行研究。

阅读量:2016

点赞量:0

收藏量:0