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

说明

玩转 webpack 学习笔记

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

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

常见的几种模块化方式

ES module:

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

CJS:

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

AMD:

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

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

在这里插入图片描述

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

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

在这里插入图片描述

npm i babel-preset-env

在这里插入图片描述

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'
    }
}

4、添加 src 入口文件

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

index.js 文件

import {
    
     hello } from './common/kaimo666.js';

document.write(hello('kaimo666'));

kaimo666.js 文件

export function hello(name) {
    
    
    return `hello ${
      
      name}`;
}

结构如下:

在这里插入图片描述

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();

然后实现 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("打包完毕");
        });
    }
}

最后实现 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;
    }
}

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

6、添加脚本进行打包

在 package.json 里添加下面脚本

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

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

npm run build

在这里插入图片描述
打包完成之后我们可以看到多了一个 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>Document</title>
</head>
<body>
    <script src="./kaimo.js"></script>
</body>
</html>

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

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

import {
    
     hello } from './common/kaimo666.js';

document.write(hello('凯小默 kaimo777'));

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

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/kaimo313/article/details/126461674
今日推荐