mini webpack

mini webpack

编写配置文件

ts 复制代码
export default {
    entry: './main.js',
    output: {
        filename: 'build.js',
        path: path.resolve(process.cwd(), 'dist')
    }
}

编写打包函数

ts 复制代码
function createAssets(filePath: string) {
    const content = fs.readFileSync(filePath, 'utf-8') // 读取入口文件代码
    const ast = parse(content, {
        sourceType: 'module'
    })
    const deps: string[] = []
    traverse(ast, {
        ImportDeclaration({ node }) {
            deps.push(node.source.value)
        }
    })
    return {
        filePath,
        deps
    }
}

babel

babel用来执行代码的转换,比如es6代码转换为更古老的es3或者es5代码

  • @babel/parser

通过这个库的parse方法将通过fs读到的js代码转成ast抽象语法树

ts 复制代码
import { parse } from '@babel/parser'
const ast = parse(content, {
    sourceType: 'module'
})
  • @babel/traverse

通过这个库遍历抽象语法树获取各个文件的依赖关系,如下

ts 复制代码
const deps: string[] = []
traverse(ast, {
    ImportDeclaration({ node }) {
        deps.push(node.source.value)
    }
})

总结 经过以上步骤,我们能够获取主文件的依赖关系如下: { filePath: './main.js', deps: ['./app.js']}

但是,如果文件层层递归的话,以上函数并不满足要求,因此我们需要编写一个函数来处理文件的多层级嵌套。

ts 复制代码
export function createGraph(entry: string): Graph[] {
    const graph = createAssets(entry, config)
    const queue = [graph]
    for (let assets of queue) {
        const deps = assets.deps
        deps.forEach((dep) => {
            const child = createAssets(dep, config)
            queue.push(child)
        })
    }
    return queue
}

总结 通过处理多层嵌套关系,得到的结果如下:

ts 复制代码
[
    { filePath: './main.js', deps: ['./app.js']},
    { filePath: './app.js', deps: ['./foo.js']},
    { filePath: './foo.js', deps: []}
]
  • @babel/core

将ast转为代码

ts 复制代码
const source = transformFromAstSync(ast, undefined, {
    presets: ['@babel/preset-env'] //es6-> es5 并且变成cjs
})

通过以上代码获取文件的代码

  • @babel/preset-env

将代码转为cjs格式的es5代码

手写require

在打包过程中,我们需要将所有的代码都整合到一个文件中,这样会导致

  1. 变量冲突 1.1 可以通过将每个文件的代码用函数封装
  2. import必须处于顶层 2.1 我们可以采用CommonJS规范的require来完成文件的导入,但是浏览器并不支持require ,因此我们需要模拟实现require
js 复制代码
function require(filePath) {
    /**
     * modules是传入的映射关系:
     * {
     *  './main.js': function(require) {}
     * }
     */
    const fn = modules(filePath)
    const module = { exports: {} }
    fn(require, module, module.exports)
    return module.exports
}

构建模板

有了以上require函数之后,我们需要构建文件路径 与代码之间的映射关系。这里采用ejs模板的方式来动态构建。

ejs 复制代码
(function(modules){ function require(filePath) { const fn = modules[filePath]; const module = { exports: {}
}; fn(require, module,
module.exports); return module.exports; } require('<%- entry %>'); })({ <% graph.forEach(item => { %> "<%- item.filePath %>":
function(require, module, exports) { <%- item.code %> })

通过@babel/core@babel/preset-env两个库获取到了代码,并将code作为数据传递给模板,最终完成了webpack的打包。代码如下:

ts 复制代码
export function build(graph: Graph[], config: Config) {
    const template = fs.readFileSync(path.join(process.cwd(), 'lib/template/bundle.ejs'), 'utf-8')
    const code = ejs.render(template, { graph, entry: 0 })
    const outDir = config.output.path
    const filename = config.output.filename
    fs.mkdirSync(config.output.path, { recursive: true })
    fs.writeFileSync(path.join(outDir, filename), code)
}

问题

由于映射对象中键是由文件的相对路径组成的,如果我们的文件处于目录中的话,可能会导致文件找不到,因此我们需要改下映射关系。改用id的方式

解决
  • 重写require
js 复制代码
function require(filePath) {
    /**
     * modules是传入的映射关系:
     * {
     *  './main.js': function(require) {}
     * }
     */
    const [fn, mapping] = modules(filePath)
    const module = { exports: {} }
    function localRequire(relativePath) {
        return require(mapping[filePath])
    }
    fn(localRequire, module, module.exports)
    return module.exports
}
  • 传递映射关系
ts 复制代码
function createAssets(filePath: string, config: Config) {
    const content = fs.readFileSync(filePath, 'utf-8') // 读取入口文件代码
    const ast = parse(content, {
        sourceType: 'module'
    })
    const deps: string[] = []
    traverse(ast, {
        ImportDeclaration({ node }) {
            deps.push(node.source.value)
        }
    })
    const source = transformFromAstSync(ast, undefined, {
        presets: ['@babel/preset-env'] //es6-> es5 并且变成cjs
    })
    return {
        filePath,
        deps,
        code: source?.code,
        id: id++,
        mapping: {} as Record<string, number>
    }
}
  • 重新构建依赖图
ts 复制代码
export function createGraph(entry: string, config: Config): Graph[] {
    const graph = createAssets(entry, config)
    const queue = [graph]
    for (let assets of queue) {
        const deps = assets.deps
        deps.forEach((dep) => {
            const child = createAssets(dep, config)
            assets.mapping[dep] = child.id
            queue.push(child)
        })
    }
    return queue
}
  • 更新模板
js 复制代码
(function(modules){ function require(filePath) { const [fn, mapping] = modules[filePath]; const module = { exports: {}
}; function localRequire(relativePath) { return require(mapping[relativePath]); } fn(localRequire, module,
module.exports); return module.exports; } require('<%- entry %>'); })({ <% graph.forEach(item => { %> "<%- item.id %>":
[function(require, module, exports) { <%- item.code %> }, <%- JSON.stringify(item.mapping) %> ], <% }) %> })

loader

  • 更新配置文件
ts 复制代码
const jsLoader = (source: string) => {
    source += 'console.log(11111)'
    return source
}
export default {
    ...
    rules: {
        module: [
            {
                test: /\.js$/,
                use: jsLoader
            }
        ]
    }
} as Config
  • 编写loader函数
ts 复制代码
import { Config } from '../type'

export function Loader(source: string, config: Config) {
    const module = config.rules.module
    module.forEach((item) => {
        if (Array.isArray(item.use)) {
            item.use.reverse().forEach((fn) => {
                source = fn(source)
            })
        } else {
            source = item.use(source)
        }
    })
    return source
}
  • 在打包之前调用
ts 复制代码
function createAssets(filePath: string, config: Config) {
    ...
    // 执行Loader
    if (source?.code) {
        source.code = Loader(source.code, config)
    }
    ...
}

plugin

  1. plugin是一个类
  2. 要有apply方法

实现HtmlWebpackPlugin

  • 编写配置文件
ts 复制代码
interface Plugin  {
    apply: (compiler: any) => void
}
interface Config {
    ...
    plugins: Plugin[]
}
export default {
    ...
    plugins: []
} as Config
  • 编写模板
ts 复制代码
import type { Hooks } from './lib/type'
class HtmlWebpackPlugin {
    apply(hooks:Hooks) {}
}

tips webpack插件的hook 是基于tapable这个库实现的 tapable是基于发布-订阅实现的

js 复制代码
import { SyncHook } from 'tapable'
const hook = new SyncHook<string>(['params'])

// 先监听,再触发
hook.tap('webpack', (name) => {
    console.log(name)
})
hook.call('xs')

基于发布-订阅 addEventListener mitt 组件通信 webpack hooks electron ipcMain ipcRender nodejs eventEmitter process

  • 自定义hook生命周期
ts 复制代码
import { AsyncSeriesHook, SyncHook } from 'tapable'
export interface Hooks<T = unknown> {
    emit: AsyncSeriesHook<T>
    afterEmit: AsyncSeriesHook<T>
    initialize: SyncHook<T>
    done: AsyncSeriesHook<T>
    afterPlugins: SyncHook<T>
}
export const hooks: Hooks = {
    afterPlugins: new SyncHook(), // 插件调用后执行
    initialize: new SyncHook(), // 插件初始化执行
    emit: new AsyncSeriesHook(), // 打包之前执行
    afterEmit: new AsyncSeriesHook(), // 打包之后执行
    done: new AsyncSeriesHook(['done']) // 打包完成执行
}
  • 编写订阅方法
ts 复制代码
class HtmlWebpackPlugin {
    options: { template: string }
    constructor(options: { template: string }) {
        this.options = options
    }
    apply(compiler: Hooks) {
        compiler.afterPlugins.tap('htmlWebpackPlugin', () => {
            console.log(1111)
        })

        compiler.done.tapPromise('htmlWebpackPlugin', () => {
            const template = fs.readFileSync(this.options.template, 'utf-8')
            const newTemplate = template.replace(/<head>/, '<head>\n<script src="bundle.js"></script>')
            fs.writeFileSync(path.resolve(process.cwd(), 'dist/index.html'), newTemplate)
            return new Promise((resolve) => {
                resolve()
            })
        })
    }
}

以上这个类实现了对于插件初始化时的监听以及打包完成的监听

  • 调用plugins中每个实例的apply方法
ts 复制代码
import type { Config } from '../type'
import { hooks } from './hook'

export const initPlugin = (config: Config) => {
    if (config.plugins) {
        config.plugins.forEach((item) => {
            item.apply(hooks)
        })
        hooks.afterPlugins.call(this)
    }
}

在构建依赖之前调用以上函数,因为是先订阅再触发,因此我们需要先执行plugins数组中实例的apply方法先进行订阅

  • 调用
ts 复制代码
export function build(graph: Graph[], config: Config) {
    ...
    // 执行打包完成的钩子,将打包后的js引入html中
    hooks.done.callAsync(graph, (error) => {
        if (error) throw error
    })
}
相关推荐
谢尔登1 天前
Webpack高级之常用配置项
前端·webpack·node.js
竹秋…1 天前
webpack搭建react开发环境
前端·react.js·webpack
越努力越幸运5081 天前
webpack的学习打包工具
前端·学习·webpack
q***71851 天前
Webpack、Vite区别知多少?
前端·webpack·node.js
谢尔登2 天前
简单聊聊webpack摇树的原理
运维·前端·webpack
谢尔登2 天前
原来Webpack在大厂中这样进行性能优化!
前端·webpack·性能优化
醉方休3 天前
Webpack loader 的执行机制
前端·webpack·rust
带只拖鞋去流浪3 天前
迎接2026,重新认识Vue CLI (v5.x)
前端·vue.js·webpack
小奶包他干奶奶4 天前
Webpack学习——Loader(文件转换器)
前端·学习·webpack
小奶包他干奶奶4 天前
Webpack学习——原理理解
学习·webpack·devops