实现⼩型Javascript打包⼯具

🌙
手机阅读
本文目录结构

问题

实现⼩型Javascript打包⼯具

答案

该⼯具可以实现以下两个功能

  • 将 ES6 转换为 ES5

  • ⽀持在 JS ⽂件中 import CSS ⽂件

通过这个⼯具的实现,⼤家可以理解到打包⼯具的原理到底是什么

实现

因为涉及到 ES6 转 ES5 ,所以我们⾸先需要安装⼀些 Babel 相关的⼯具

yarn add babylon babel-traverse babel-core babel-preset-env

接下来我们将这些⼯具引⼊⽂件中

const fs = require('fs')
const path = require('path')
const babylon = require('babylon')
const traverse = require('babel-traverse').default
const { transformFromAst } = require('babel-core')

⾸先,我们先来实现如何使⽤ Babel 转换代码

function readCode(filePath) {
    // 读取⽂件内容
    const content = fs.readFileSync(filePath, 'utf-8')
    // ⽣成 AST
    const ast = babylon.parse(content, {
        sourceType: 'module'
    })
    // 寻找当前⽂件的依赖关系
    const dependencies = []
    traverse(ast, {
        ImportDeclaration: ({ node }) => {
            dependencies.push(node.source.value)
        }
    })
    // 通过 AST 将代码转为 ES5
    const { code } = transformFromAst(ast, null, {
        presets: ['env']
    })
    return {
        filePath,
        dependencies,
        code
    }
}

⾸先我们传⼊⼀个⽂件路径参数,然后通过 fs 将⽂件中的内容读取出来

接下来我们通过 babylon 解析代码获取 AST ,⽬的是为了分析代码中是否还引⼊了别的⽂件

通过 dependencies 来存储⽂件中的依赖,然后再将 AST 转换为 ES5 代码

最后函数返回了⼀个对象,对象中包含了当前⽂件路径、当前⽂件依赖和当前⽂件转换后的代码

接下来我们需要实现⼀个函数,这个函数的功能有以下⼏点

调⽤ readCode 函数,传⼊⼊⼝⽂件

分析⼊⼝⽂件的依赖

识别 JS 和 CSS ⽂件

function getDependencies(entry) {
    // 读取⼊⼝⽂件
    const entryObject = readCode(entry)
    const dependencies = [entryObject]
    // 遍历所有⽂件依赖关系
    for (const asset of dependencies) {
        // 获得⽂件⽬录
        const dirname = path.dirname(asset.filePath)
        // 遍历当前⽂件依赖关系
        asset.dependencies.forEach(relativePath => {
        // 获得绝对路径
            const absolutePath = path.join(dirname, relativePath)
            // CSS ⽂件逻辑就是将代码插⼊到 `style` 标签中
            if (/\.css$/.test(absolutePath)) {
                const content = fs.readFileSync(absolutePath, 'utf-8')
                const code = `
                const style = document.createElement('style')
                style.innerText = ${JSON.stringify(content).replace(/\\r\\n/g, ''
                document.head.appendChild(style)
                `
                dependencies.push({
                    filePath: absolutePath,
                    relativePath,
                    dependencies: [],
                    code
                })
                } else {
                    // JS 代码需要继续查找是否有依赖关系
                    const child = readCode(absolutePath)
                    child.relativePath = relativePath
                    dependencies.push(child)
                }
            })
        }
        return dependencies
    }

⾸先我们读取⼊⼝⽂件,然后创建⼀个数组,该数组的⽬的是存储代码中涉及到的所有⽂ 件

接下来我们遍历这个数组,⼀开始这个数组中只有⼊⼝⽂件,在遍历的过程中,如果⼊⼝ ⽂件有依赖其他的⽂件,那么就会被 push 到这个数组中

在遍历的过程中,我们先获得该⽂件对应的⽬录,然后遍历当前⽂件的依赖关系

在遍历当前⽂件依赖关系的过程中,⾸先⽣成依赖⽂件的绝对路径,然后判断当前⽂件是 CSS ⽂件还是 JS ⽂件

如果是 CSS ⽂件的话,我们就不能⽤ Babel 去编译了,只需要读取 CSS ⽂件中的代 码,然后创建⼀个 style 标签,将代码插⼊进标签并且放⼊ head 中即可

如果是 JS ⽂件的话,我们还需要分析 JS ⽂件是否还有别的依赖关系

最后将读取⽂件后的对象 push 进数组中

现在我们已经获取到了所有的依赖⽂件,接下来就是实现打包的功能了

function bundle(dependencies, entry) {
    let modules = ''
    // 构建函数参数,⽣成的结构为
    // { './entry.js': function(module, exports, require) { 代码 } }
    dependencies.forEach(dep => {
        const filePath = dep.relativePath || entry
        modules += `'${filePath}': (
            function (module, exports, require) { ${dep.code} }
        ),`
    })
    // 构建 require 函数,⽬的是为了获取模块暴露出来的内容
    const result = `
        (function(modules) {
            function require(id) {
            const module = { exports : {} }
            modules[id](module, module.exports, require)
            return module.exports
        }
        require('${entry}')
        })({${modules}})
    // 当⽣成的内容写⼊到⽂件中
    fs.writeFileSync('./bundle.js', result)
}

这段代码需要结合着 Babel 转换后的代码来看,这样⼤家就能理解为什么需 要这样写了

// entry.js
var _a = require('./a.js')
var _a2 = _interopRequireDefault(_a)
function _interopRequireDefault(obj) {
    return obj && obj.__esModule ? obj : { default: obj }
}
console.log(_a2.default)
// a.js
Object.defineProperty(exports, '__esModule', {
    value: true
})
var a = 1
exports.default = a

Babel 将我们 ES6 的模块化代码转换为了 CommonJS 的代码,但是浏览器 是不⽀持 CommonJS 的,所以如果这段代码需要在浏览器环境下运⾏的话, 我们需要⾃⼰实现 CommonJS 相关的代码,这就是 bundle 函数做的⼤部 分事情。

接下来我们再来逐⾏解析 bundle 函数

⾸先遍历所有依赖⽂件,构建出⼀个函数参数对象

对象的属性就是当前⽂件的相对路径,属性值是⼀个函数,函数体是当前⽂件下的代码, 函数接受三个参数 module 、 exports 、 require module 参数对应 CommonJS 中的 module exports 参数对应 CommonJS 中的 module.export require 参数对应我们⾃⼰创建的 require 函数

接下来就是构造⼀个使⽤参数的函数了,函数做的事情很简单,就是内部创建⼀个 require 函数,然后调⽤ require(entry) ,也就是 require(’./entry.js’) ,这样

就会从函数参数中找到 ./entry.js 对应的函数并执⾏,最后将导出的内容通过 module.export 的⽅式让外部获取到

最后再将打包出来的内容写⼊到单独的⽂件中

如果你对于上⾯的实现还有疑惑的话,可以阅读下打包后的部分简化代码

;(function(modules) {
    function require(id) {
        // 构造⼀个 CommonJS 导出代码
        const module = { exports: {} }
        // 去参数中获取⽂件对应的函数并执⾏
        modules[id](module, module.exports, require)
        return module.exports
    }
    require('./entry.js')
})({
    './entry.js': function(module, exports, require) {
    // 这⾥继续通过构造的 require 去找到 a.js ⽂件对应的函数
    var _a = require('./a.js')
    console.log(_a2.default)
    },
    './a.js': function(module, exports, require) {
    var a = 1
    // 将 require 函数中的变量 module 变成了这样的结构
    // module.exports = 1
    // 这样就能在外部取到导出的内容了
    exports.default = a
    }
    // 省略
})

虽然实现这个⼯具只写了不到 100 ⾏的代码,但是打包⼯具的核⼼原理就是 这些了

  • 找出⼊⼝⽂件所有的依赖关系

  • 然后通过构建 CommonJS 代码来获取 exports 导出的内容

更多面试题

如果你想了解更多的前端面试题,可以查看本站的WEB前端面试题 ,这里基本包涵了市场上的所有前端方面的面试题,也有一些大公司的面试图,可以让你面试更加顺利。

面试题
HTML CSS JavaScript
jQuery Vue.js React
算法 HTTP Babel
BootStrap Electron Gulp
Node.js 前端经验相关 前端综合
Webpack 微信小程序 -

这些题库还在更新中,如果你有不错的面试题库欢迎分享给我,我整理后放上来;人人为我,我为人人,互帮互助,共同提高,祝大家都拿到心仪的Offer!

AXIHE / 精选资源

浏览全部教程

面试题

学习网站

前端培训
自己甄别

前端书籍

关于朱安邦

我叫 朱安邦,阿西河的站长,在杭州。

以前是一名平面设计师,后来开始接接触前端开发,主要研究前端技术中的JS方向。

业余时间我喜欢分享和交流自己的技术,欢迎大家关注我的 Bilibili

关注我: Github / 知乎

于2021年离开前端领域,目前重心放在研究区块链上面了

我叫朱安邦,阿西河的站长

目前在杭州从事区块链周边的开发工作,机械专业,以前从事平面设计工作。

2014年底脱产在老家自学6个月的前端技术,自学期间几乎从未出过家门,最终找到了满意的前端工作。更多>

于2021年离开前端领域,目前从事区块链方面工作了