前端养成

day04: 手写简单的webpack

2021-06-09  本文已影响0人  云海成长

开发思路:

  1. 创建入口文件
  2. 提取该入口文件的所有依赖
  3. 解析入口文件依赖的依赖,递归解析文件的依赖形成关系图,描述所有文件的依赖关系
  4. 把所有文件打包成一个文件

废话不多说,从零开始!

开发准备

  1. 创建文件夹mini-webpack
  2. mini-webpack下运行yarn init -y
  3. 新建README.MD
  4. 创建几个js文件

index.js ,作为入口文件

import add from '../util/add.js'
import minus from './minus.js'
import common from '../util/common.js'

add(1, 4)
minus(6, 8)
common('测试')

minus.js

import common from '../util/common.js'
export default (a, b) => {
    common(a - b)
}

add.js

import common from './common.js'
export default (a, b) => {
    common(a + b)
}

common.js

export default (res) => {
    console.log(`the result is ${res}`)
}

index.jsminus.js放在src下, add.jscommon.js放在util文件夹下。

创建webpack.js作为编写打包核心代码的文件

完成结果:

结果

开始开发

做好以上准备,我们就可以开始开发了。

按照思路,要先获取入口文件的依赖

1. 先获取文件内容,看是否正确

// webpack.js
const fs = require('fs')
const content = fs.readFileSync('./src/index.js', 'utf-8') //  获取文件内容
console.log(content)
文件内容
拿到入口文件的内容了,那么怎么去得到它的依赖呢

打开网站https://astexplorer.net/#/2uBU1BLuJ1,将index.js的内容粘贴到上面,将入口文件的内容转为AST树,通过观察可以发现:

入口文件AST
ImportDeclaration类型的节点的sourcevalue属性包含文件的依赖,所以,下一步要将内容转为AST树,这一步需要借助工具@babel/parser

2. 将内容转为AST树

首先安装@babel/parser

yarn add -D @babel/parser
const fs = require('fs')
const parser = require('@babel/parser')
const content = fs.readFileSync('./src/index.js', 'utf-8') //  获取文件内容
const ast = parser.parse(content, {
    sourceType: 'module', // 识别ES Module
  })
console.log(ast)

运行结果:


image.png

3. 遍历AST树获取依赖

成功转换,下一步当然是要获取ImportDeclaration类型的节点,即需要遍历AST树,需要借助@babel/traverse
安装:

yarn add @babel/traverse -D

获取依赖

const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const content = fs.readFileSync('./src/index.js', 'utf-8') //  获取文件内容
const ast = parser.parse(content, {
    sourceType: 'module', // 识别ES Module
  })
// 存储依赖
  const dependencies = []
  // 为了获取文件的依赖, 需要能够遍历ast,拿到ImportDeclaration节点, 于是引入@babel/traverse
  traverse(ast, {
    ImportDeclaration({ node }) {
      dependencies.push(node.source.value)
    },
  })
console.log(dependecies )

运行结果:


image.png

成功获取入口文件的依赖

4. 获取依赖图

上面我们已经能够获取一个文件的依赖的,现在我们要获取依赖的依赖,递归解析形成依赖图
将上面获取一个文件依赖的内容封装成一个方法,且为了区分不同的依赖,添加全局变量ID,如下

const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
let ID = 0 
// 获取一个文件的依赖
function creatAsset(filePath) {
  // 读取文件的内容
  const content = fs.readFileSync(filePath, 'utf-8')
  // 为了文件的依赖,借助babylon将内容转为AST, 参考:https://astexplorer.net/#/2uBU1BLuJ1
  const ast = parser.parse(content, {
    sourceType: 'module', // 识别ES Module
  })
  // 存储依赖
  const dependencies = []
  // 为了获取文件的依赖, 需要能够遍历ast,拿到ImportDeclaration节点, 于是引入@babel/traverse
  traverse(ast, {
    ImportDeclaration({ node }) {
      dependencies.push(node.source.value)
    },
  })
  return {
    id: ID++,
    filePath,
    dependencies
  }
}

于是我们创建一个方法createGraph,用来创建依赖图

function createGraph(entry) {
 const mainAsset = creatAsset(entry)
  let graph = [mainAsset]
  for (let asset of graph) {
    const dir = path.dirname(asset.filePath)
    asset.mapping = {}
    for (let relativePath of asset.dependencies) {
      // 这里做路径转化,让被依赖的文件相对与当前文件filePath,而不是webpack.js
      let childAsset = creatAsset(path.join(dir, relativePath))
      // 由于数组是动态的,这一步可以让数组遍历新推进的元素childAsset
      graph.push(childAsset)
      asset.mapping[relativePath] = childAsset.id
    }
  }
  return graph
}
const graph = createGraph('./src/index.js')
console.log(graph)

对于以上方法,首先传入入口文件entry, 获得mainAsset ,包含入口文件的id, filepath,和dependencies, 将mainAsset作为graph的第一个节点,遍历这个图,获得节点的依赖(即文件路径数组),通过creatAsset递归地获取依赖的依赖,由于存在路径可能是相对路径,所以需要将路径转化成绝对路径,从而正确加载模块。mapping记录相对路径和资源id的映射关系,最后执行打印一下结果

image.png

已经获得所有的依赖关系,但是仅仅是文件路径,所以我们还要加入编译内容

5. 编译文件内容

这一步需要借助@babel/core@babel/preset-env
安装

yarn add -D @babel/core @babel/preset-env

creatAsset里加入代码编译

// 获取一个文件的依赖
function creatAsset(filePath) {
  // 读取文件的内容
  const content = fs.readFileSync(filePath, 'utf-8')
  // 为了文件的依赖,借助babylon将内容转为AST, 参考:https://astexplorer.net/#/2uBU1BLuJ1
  const ast = parser.parse(content, {
    sourceType: 'module', // 识别ES Module
  })
  // 存储依赖
  const dependencies = []
  // 为了获取文件的依赖, 需要能够遍历ast,拿到ImportDeclaration节点, 于是引入@babel/traverse
  traverse(ast, {
    ImportDeclaration({ node }) {
      dependencies.push(node.source.value)
    },
  })

  // 编译@babel/core,参考https://babeljs.io/repl#?browsers=&build=&builtIns=false&corejs=3.6&spec=false&loose=false&code_lz=JYWwDg9gTgLgBAQwB7AgZzgMyhEcDkyqa-A3AFDkCmSkscAJlZggK4A28R6pQA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=true&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=env&prettier=true&targets=&version=7.14.4&externalPlugins=
  // @babel/preset-env 编译成什么格式
  // 注意babel应该配套使用,不然会因为版本不同而报错
  // 将ast编译成预设格式的js代码
  const { code } = babel.transformFromAstSync(ast, null, {
    presets:["@babel/preset-env"]
  })
  return {
    id: ID++,
    filePath,
    dependencies,
    code
  }
}

再次打印


image.png

拿到编译后的代码,开始准备打包

把所有文件打包成一个文件

编译后的代码里包含require,module, exports, 所以要将编译后的模块代码分别用function(require, module, exports) {}包裹起来,并且自行实现这3个属性,传入函数

// 打包
function bundle (graph) {
    let modules = ''
    graph.forEach(module => {
        modules += `${module.id}: [function (require, module, exports) {
            ${module.code}
        }, ${JSON.stringify(module.mapping)}],`
    })
    return `(function(modules) {
        function require(id) {
            const [fn, mapping] = modules[id]
            const module = {exports: {} }
            function localRequie(relativePath) { // 由于在文件内,使用import通过文件名称引入,但是我们自定义的require使用的是id,所以使用模块的mapping做一个转换
                return require(mapping[relativePath])
            }
            fn(localRequie, module, module.exports)
            return module.exports
        }
        // 调用入口
        require(0)
    })({${modules}})`
}

在package.json里添加命令:

"scripts": {
        "build": "node webpack.js > dist.js"
    }

测试: yarn build
将dist.js里面的内容粘贴到浏览器执行,

image.png

至此,一个简单的打包工具就完成了。
源码地址: https://github.com/fanqingyun/mini-webpack

上一篇下一篇

猜你喜欢

热点阅读