webpack打包原理&手写webpack核心打包过程

2,928 阅读3分钟

webpack

webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)

需要整理的

  • 打包体积缩减
  • 分包
  • DLL
  • 手动配两个环境,三个配置文件(base、dev、prod)
  • 编译加速
  • dev-server + mock

webpack介绍

当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

webpack功能

  • 代码转换
  • 文件优化
  • 代码分割
  • 模块合并
  • 自动刷新
  • 代码校验
  • 自动发布

有用的链接

link:Babel中文文档

webpack + vue脚手架

常用配置项

  • 配置文件
    • 多种配置类型
    • 入口
    • 输出
    • hash、chunkhash、contenthash
    • 模式
    • 自动生成页面
  • loader
    • css-loader和style-loader
    • sass-loader和less-loader
    • postcss-loader 自动加-moz、-ms、-webkit等浏览器私有前缀
    • babel-loader
    • file-loader和url-loader
    • html-withimg-loader
    • vue-loader
  • 搭建开发环境
    • plugins
    • clean-webpack-plugin
    • mini-css-extract-plugin
    • optimize-css-assets-webpack-plugin
    • copy-webpack-plugin
    • ProvidePlugin
    • loader和plugin的区别

手写webpack核心打包过程

1. 打包的主要流程

  • 需要读到入口文件里面的内容
  • 分析入口文件,递归的去读取模块所依赖的文件内容,生成AST语法树
  • 根据AST语法树,生成浏览器能够运行的代码

2. 打包过程的步骤

  • 获取模块内容
  • 分析模块
  • 收集依赖
  • ES6转成ES5(AST)
  • 递归获取所有依赖
  • 处理两个关键字(import、exports)
  • 输出打包结果代码

3. 实现打包的步骤

  • 获取主模块内容
  • 分析模块
    • 安装@babel/parser包(转AST)
  • 对模块内容进行处理
    • 安装@babel/traverse包(遍历AST收集依赖)
    • 安装@babel/core和@babel/preset-env包 (es6转ES5)
  • 递归所有模块
  • 生成最终代码

4. 代码

/**
 * 手写webpack核心打包流程
 */
// 获取主入口文件
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');

// 解析单个文件,获取文件路径,依赖文件列表,编译成es5的代码
const getModuleInfo = (file) => {
    const body = fs.readFileSync(path.resolve(__dirname, file), 'utf-8');
    // 新增代码
    const ast = parser.parse(body, {
        sourceType: 'module' // 表示我们要解析的是ES模块
    });

    // 新增代码
    const deps = {};
    traverse(ast, {
        ImportDeclaration({node}) {
            const dirname = path.dirname(file);
            const abspath = './' + path.join(dirname, node.source.value);
            deps[node.source.value] = abspath;
        }
    });
    const {code} = babel.transformFromAst(ast, null, {
        presets: ['@babel/preset-env']
    });
    const moduleInfo = {file, deps, code};
    return moduleInfo;
};
// 编译入口文件,非递归遍历AST依赖树,将所有文件解析后生成平铺的映射对象
const parseModules = (file) => {
    const entry = getModuleInfo(file);
    const temp = [entry];
    const depsGraph = {};
    for (let i = 0; i < temp.length; i++) {
        const deps = temp[i].deps;
        if (deps) {
            for (const key in deps) {
                if (deps.hasOwnProperty(key)) {
                    temp.push(getModuleInfo(deps[key]));
                }
            }
        }
    }

    temp.forEach(moduleInfo => {
        depsGraph[moduleInfo.file] = {
            deps: moduleInfo.deps,
            code: moduleInfo.code
        };
    });
    return depsGraph;
};

// 打包,生成递归遍历执行依赖关系树的执行器和AST依赖关系树
const bundle = (file) => {
    const depsGraph = JSON.stringify(parseModules(file), null, 4);
    return `;(function (graph) {
        function require(file) {
            function absRequire(relPath) {
                return require(graph[file].deps[relPath])
            }
            var exports = {};
            (function (require, exports, code) {
                eval(code);
            })(absRequire, exports, graph[file].code);
            return exports;
        }
        require('${file}');
    })(${depsGraph});`;

};

const entry = './src/index.js';
const content = bundle(entry);

console.log(content);

// 输出代码
if (!fs.existsSync('./dist')) {
    fs.mkdirSync('./dist');
}
fs.writeFileSync('./dist/bundle.js', content);

参考&转载: