美文网首页前端养成
day04: 手写简单的webpack

day04: 手写简单的webpack

作者: 云海成长 | 来源:发表于2021-06-09 17:48 被阅读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

    相关文章

      网友评论

        本文标题:day04: 手写简单的webpack

        本文链接:https://www.haomeiwen.com/subject/baprsltx.html