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

Posted 凯小默

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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'));

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

以上是关于webpack原理篇(五十八):实战开发一个简易的webpack的主要内容,如果未能解决你的问题,请参考以下文章

webpack原理篇(五十五):webpack流程:准备阶段

webpack原理篇(五十七):webpack流程:文件生成

webpack原理篇(五十九):loader 的链式调用与执行顺序

webpack原理篇(五十四):Tapable是如何和webpack进行关联起来的?

webpack原理篇(五十二):webpack-cli源码阅读

webpack原理篇(五十六):webpack流程:模块构建和chunk生成阶段