• webpack原理篇(五十八):实战开发一个简易的webpack


    说明

    玩转 webpack 学习笔记

    模块化:增强代码可读性和维护性

    1. 传统的网页开发转变成 Web Apps 开发
    2. 代码复杂度在逐步增高
    3. 部署时希望把代码优化成几个 HTTP 请求
    4. 分离的 JS文件/模块,便于后续代码的维护性

    常见的几种模块化方式

    ES module:

    import * as largeNumber from 'large-number';
    // ...
    largeNumber.add('999', '1');
    
    • 1
    • 2
    • 3

    CJS:

    const largeNumbers = require('large-number');
    // ...
    largeNumber.add('999', '1');
    
    • 1
    • 2
    • 3

    AMD:

    require(['large-number'], function (large-number) {
    	// ...
    	largeNumber.add('999', '1');
    });
    
    • 1
    • 2
    • 3
    • 4

    AST 基础知识

    抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。

    在线:demo: https://esprima.org/demo/parse.html

    在这里插入图片描述

    在这里插入图片描述

    webpack 的模块机制

    • 打包出来的是一个 IIFE (匿名闭包)
    • modules 是一个数组,每一项是一个模块初始化函数
    • __webpack_require 用来加载模块,返回 module.exports
    • 通过 WEBPACK_REQUIRE_METHOD(0) 启动程序

    在这里插入图片描述

    实现一个简易的 webpack

    可以将 ES6 语法转换成 ES5 的语法,生成的 JS 文件可以在浏览器中运行

    • 通过 babylon 生成AST
    • 通过 babel-core 将AST重新生成源码

    可以分析模块之间的依赖关系

    • 通过 babel-traverse 的 ImportDeclaration 方法获取依赖属性

    1、新建初始化项目

    新建 mini-webpack 文件夹,执行下面命令,初始化项目

    npm init -y
    
    • 1

    在这里插入图片描述

    2、安装相关依赖

    • babylon:使用 babylon 生成AST
    • babel-core:使用 babel-core 将AST重新生成源码
    • babel-traverse:使用 babel-traverse 的 ImportDeclaration 方法获取依赖属性
    • babel-preset-env:通过根据目标浏览器或运行时环境自动确定所需的 Babel 插件和 polyfill,将 ES2015+ 编译为 ES5 的 Babel 预设。
    npm i babylon babel-core babel-traverse
    
    • 1

    在这里插入图片描述
    这里需要安装下面这个插件,不安装到时会报错

    在这里插入图片描述

    npm i babel-preset-env
    
    • 1

    在这里插入图片描述

    3、添加 minipack.config.js 配置文件

    里面模仿 webpack 的配置

    const path = require('path');
    
    module.exports = {
        // 入口
        entry: path.join(__dirname, './src/index.js'),
        // 输出文件
        output: {
            path: path.join(__dirname, './dist'),
            filename: 'kaimo.js'
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    4、添加 src 入口文件

    新建 src,里面添加 index.js 文件,里面依赖 common 文件夹里的 kaimo666.js 里的方法

    index.js 文件

    import { hello } from './common/kaimo666.js';
    
    document.write(hello('kaimo666'));
    
    • 1
    • 2
    • 3

    kaimo666.js 文件

    export function hello(name) {
        return `hello ${name}`;
    }
    
    • 1
    • 2
    • 3

    结构如下:

    在这里插入图片描述

    5、实现 mini-webpack 的核心功能

    新建 lib 文件夹,首先添加 index.js 文件,到时执行 node ./lib/index.js 就可以进行编译打包了。

    // 编译模块
    const Compiler = require('./compiler.js');
    // 获取配置
    const options = require('../minipack.config.js');
    // 实例化 compiler
    new Compiler(options).run();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后实现 compiler.js 功能里面需要结束 config 的配置,以及 run 去执行。

    const { getAst, getDependencis, transform } = require("./parser.js");
    const path = require('path');
    const fs = require('fs');
    
    module.exports = class Compiler {
        constructor(options) {
            const { entry, output } = options;
            this.entry = entry;
            this.output = output;
            this.modules = [];
        }
        run() {
            // 从入口文件开始构建
            const entryModule = this.buildModule(this.entry, true);
            this.modules.push(entryModule);
            // 遍历模块依赖进行构建
            this.modules.map(_module => {
                _module.dependencies.map(dependency => {
                    this.modules.push(this.buildModule(dependency));
                })
            })
            // 构建完成输出文件
            this.emitFiles();
        }
        /**
         * 构建模块:用于获取文件的路径,ast,相关依赖
         * @param filename 文件路径
         * @param isEntry 是否是入口文件
         * */ 
        buildModule(filename, isEntry) {
            let ast;
            if(isEntry) {
                ast = getAst(filename);
            } else {
                // 获取文件的绝对路径:process.cwd()是指当前node命令执行时所在的文件夹目录
                let absolutePath = path.join(process.cwd(), './src', filename);
                ast = getAst(absolutePath);
            }
    
            return {
                filename,
                dependencies: getDependencis(ast),
                transformCode: transform(ast)
            }
        }
        // 输出文件
        emitFiles() {
            // 输出的文件路径
            const outputPath = path.join(this.output.path, this.output.filename);
            // 组装依赖的 modules
            let modules = '';
            this.modules.map(_module => {
                modules += `'${_module.filename}': function (require, module, exports) { ${_module.transformCode} },`
            })
            // 组装生成的代码 bundle
            const bundle = `
                (function(modules){
                    function require(fileName) {
                        const fn = modules[fileName];
                        const module = { exports: {} };
                        fn(require, module, module.exports);
                        return module.exports;
                    }
                    require('${this.entry}');
                })({${modules}})
            `;
            console.log("emitFiles--->", outputPath, bundle)
            // recursive: true 参数,不管创建的目录是否存在
            fs.mkdir(this.output.path, { recursive: true }, function(err) {
                if (err) throw err;
                console.log("目录创建成功");
                // 使用 fs.writeFileSync 将数据同步写入文件
                fs.writeFileSync(outputPath, bundle, 'utf-8');
                console.log("打包完毕");
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    最后实现 parser 里的相关方法

    const fs = require('fs');
    const babylon = require('babylon');
    const { default: traverse } = require('babel-traverse');
    const { transformFromAst } = require('babel-core');
    
    module.exports = {
        // 获取文件的 ast
        getAst: path => {
            // 同步读取文件
            console.log("getAst----path>", path)
            const content = fs.readFileSync(path, 'utf-8');
            console.log("getAst---->", content)
            // 分析AST,从中得到 import 的模块信息(路径)
            return babylon.parse(content, {
                sourceType: 'module'
            })
        },
        // 获取文件的依赖
        getDependencis: ast => {
            const dependencies = [];
            traverse(ast, {
                // ImportDeclaration 方法:当遍历到 import 时的一个回调
                ImportDeclaration: ({ node }) => {
                    // 将依赖 push 到 dependencies 中
                    dependencies.push(node.source.value);
                }
            });
            return dependencies;
        },
        transform: ast => {
            // es6 转化为 es5
            const { code } = transformFromAst(ast, null, {
                presets: ['env']
            });
            return code;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    结构如下:
    在这里插入图片描述

    6、添加脚本进行打包

    在 package.json 里添加下面脚本

    "build": "node ./lib/index.js"
    
    • 1

    在这里插入图片描述
    然后我们执行

    npm run build
    
    • 1

    在这里插入图片描述
    打包完成之后我们可以看到多了一个 dist 的文件夹,里面有打包好的 kaimo.js 文件

    在这里插入图片描述

    7、测试打包好的文件能否正常运行

    我们在 dist 文件夹下面添加 index.html 文件,添加下面代码

    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Documenttitle>
    head>
    <body>
        <script src="./kaimo.js">script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    浏览器访问 index.html 文件,效果如下
    在这里插入图片描述

    我们改动一下 src 下 index.js 的代码

    import { hello } from './common/kaimo666.js';
    
    document.write(hello('凯小默 kaimo777'));
    
    • 1
    • 2
    • 3

    然后打包,成功之后刷新页面,我们可以看到效果也变了。

    在这里插入图片描述

  • 相关阅读:
    全国世界城市列表API接口,免费好用
    3D目标检测进展综述(论文笔记)
    CYaRon!语
    【云原生 | Kubernetes 系列】K8s 实战 Kubernetes 声明式对象的 增 删 改 查
    C语言练习百题之9的次数
    Lab1:练习4——分析bootloader加载ELF格式的OS的过程
    【LeetCode】螺旋矩阵&&旋转图像
    Java 世界的法外狂徒:反射
    2022DASCTF7月赋能赛(复现)
    opencv c++ 实时对象追踪
  • 原文地址:https://blog.csdn.net/kaimo313/article/details/126461674