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
    })
}
相关推荐
ak啊5 小时前
Webpack 构建阶段:模块解析流程
前端·webpack·源码
前端与小赵6 小时前
webpack和vite之间的区别
前端·webpack·vite
Moment19 小时前
从 Webpack 源码来深入学习 Tree Shaking 实现原理 🤗🤗🤗
前端·javascript·webpack
henujolly20 小时前
优化webpack打包体积思路
webpack
EricXJ21 小时前
为什么选择 tsup?
前端·webpack·typescript
SuperherRo1 天前
Web开发-JS应用&WebPack构建&打包Mode&映射DevTool&源码泄漏&识别还原
前端·javascript·webpack·源码泄露·识别还原
小浣熊喜欢揍臭臭1 天前
webpack配置详解+项目实战
前端·webpack·node.js
晴空9692 天前
对webpack工程化的理解
webpack
蓝桉柒72 天前
安装Webpack并创建vue项目
前端·vue.js·webpack
梦想CAD控件3 天前
(在线CAD集成)网页CAD二次开发中配置属性的详细教程
前端·javascript·webpack