webpack系列-进阶

编写一个简单的Loader

参考文档

loader api: https://webpack.docschina.org/api/loaders/

resolveLoader: https://webpack.docschina.org/configuration/resolve/#resolveloader

初始化简单的项目
npm init -y

npm install --save-dev webpack webpack-cli
创建loader

loader本质就是函数

创建完成本地的loader之后,在本地项目中使用webpack.config.js

const path = require('path');

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  module: {
    rules: [
      {
        test: /\.js/,
        use: [
          // 使用本地loader
          path.resolve(__dirname, './loaders/replaceLoader.js')
        ]
      }
    ]
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}

使用更多的配置项,让loader中可以获取到配置项内容:

// webpack.config.js
module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.js/,
        use: [
          // 使用本地loader
          {
            loader: path.resolve(__dirname, './loaders/replaceLoader.js')
            options: { // loader中可以通过this.query获取配置内容
              name: 'dellLee'
            }
          }
        ]
      }
    ]
  }
  ...
}

// loader.js
module.exports = function(source) {
  console.log(this.query) // { name: 'dellLee }
  return source.replace('dell', this.query.name)
}

需要返回多余的信息使用this.callback,使用this.async进行异步操作

// loader中使用this.async进行处理
module.exports = function(source) {
    const options = this.getOptions()
    const callback = this.async() // this.async返回一个this.callback
    
    setTimeout(() => {
        const result = source.replace('dell', opthions.name)
        callback(null, result)
    }, 1000)
}

使用resolveLoader用于解析本地的webpack loader

module.exports = {
    resolveLoader: {
        modules: ['node_modules', './loaders'], // 会先在node_modules查找loader,如果查找不到则会到第二个配置查找
       extensions: ['.js', '.json'],  
    },
    module: {
        use: [
            // 配合resolveLoader本地loader使用
            {
                loader: 'replaceLoader',
                options: { // loader中可以通过this.query获取配置内容
                  name: 'dellLee'
                }
            }
        ]
    }
}

可以使用loader做全局处理,如国际化、错误报警等。

编写一个简单的plugin

plugin的核心机制:事件驱动,发布订阅的设计模式。

参考文档

Plugins: https://webpack.docschina.org/api/compiler-hooks/

钩子类型描述:https://github.com/webpack/tapable#tapable

创建插件plugin

plugin的本质是对象

创建一个简单的plugin用于打印copyright一些信息:

class CopyrightWebpackPlugin {
    constructor(options) {
        console.log('插件被使用了')
    }
    
    apply(compiler) { // compiler存放打包的内容和配置
        // 输出asset到output目录之前 异步执行
        compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
            // compilation存放此次打包的内容
            compilation.assets['copyright.txt'] = { // 生成copyright文件
                source: function() {
                    return 'copyright by dell lee'
                },
                size: function() {
                    return 21
                }
            }
            cb()
        })
    }
}

module.exports = CopyrightWebpackPlugin;

compilation.assets中的内容:

plugin_compilation_assets

webpack.config.js使用本地创建的plugin:

const path = require('path');
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin.js')

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  plugins: {
      new CopyrightWebpackPlugin({
          name: 'hello'
      }) // 实例化插件
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}

调试

参考官网文章:https://www.webpackjs.com/contribute/debugging/

安装node-nightlynpm install node-nightly -g

安装完成后,需要在命令行中运行一次,结束安装:node-nightly

可以直接使用带有 --inspect 标记的 node-nightly,在任何基于 webpack 的项目中开始构建。注意,我们不应该运行 NPM scripts,例如 npm run build,所以我们需要指定完整的 node_modules 路径:

node-nightly --inspect ./node_modules/webpack/bin/webpack.js

应该输出如下内容:

Debugger listening on ws://127.0.0.1:9229/c624201a-250f-416e-a018-300bbec7be2c
For help see https://nodejs.org/en/docs/inspector

现在,在浏览器中访问 chrome://inspect,你会看到在 Remote Target 标题下可以进行 inspect(审查) 的活动脚本。单击每个脚本下自动连接会话的 “inspect” 链接,打开一个专门 debugger 或 Open dedicated DevTools for Node 链接。你还可以看到 NiM 扩展程序,这是一个方便的 Chrome 插件,在每次你通过 --inspect 调试某个脚本时,都会自动打开 DevTools 标签页。

我们推荐使用 --inspect-brk 标记,此标记将在脚本的第一条语句处断开,以便你可以在源代码中设置断点,并根据需要启动/停止构建。此外,不要忘记,你仍然可以向脚本传递参数。例如,如果你有多个配置文件,你可以通过 --config webpack.prod.js 指定你想要调试的配置。

其他参考链接

node debugging: https://nodejs.org/zh-cn/docs/guides/debugging-getting-started/

创建一个简易的bundler库

简单的ESM转CJS,更多模块化知识

项目仓库

// index.js为仅简单的模块间相互调用的示例

// 简易的打包工具
// 对项目的打包,首先要先读取入口文件,对项目代码进行分析
// cli-highlight插件,帮助高亮cli输出
// @babel/parser 帮助进行源代码语义分析 https://www.babeljs.cn/docs/babel-parser
// @babel/traverse 对解析出的抽象语法树进行遍历 https://www.babeljs.cn/docs/babel-traverse
// @babel/core babel的核心模块,对代码进行转义,使得可以在浏览器中运行 https://babeljs.io/docs/en/babel-core
// @babel/preset-env代码转义插件,将代码转换为目标环境可运行的代码  https://babeljs.io/docs/en/babel-preset-env

const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default; // 默认ES6 module的导出,使用要用default
const babel = require('@babel/core')

const moduleAnalyser = (filename) => { // 读取文件,获取文件内容 进行解析抽象语法树
    const content = fs.readFileSync(filename, 'utf-8');
    // 获取抽象语法树
    const ast = parser.parse(content, {
        sourceType: 'module'
    })
    const dependencies = {} // 获取依赖文件信息, 存入的路径是绝对路径或者是相对于bundle的相对路径
    // 如果只存一个相对路径会在打包的时候比较麻烦,所以存一个相对路径和绝对路径
    traverse(ast, {
        ImportDeclaration({ node }) {
            const dirname = path.dirname(filename)
            const sourceValue = node.source.value
            const newFile = './' + path.join(dirname, sourceValue)
            dependencies[sourceValue] = newFile; // 键值对的方式进行存储
        }
    });
    // 对抽象语法树进行转换, 将转换后的code取出
    const { code } = babel.transformFromAst(ast, null, {
        presets: ['@babel/preset-env']
    })
    return {
        filename, // 文件
        dependencies, // 依赖
        code // 转移后的代码
    }
}

// 依赖图谱,将入口文件中的依赖文件再进一步分析,一层一层递进
const makeDependenciedGraph = (entry) => {
    // 分析入口文件
    const entryModule = moduleAnalyser(entry);
    const graphArray = [ entryModule ]; // 初始化图谱数组

    for (let i = 0; i < graphArray.length; i++) {
        const item = graphArray[i];
        const { dependencies } = item; // 获取依赖文件
        if (dependencies) {
            // 如果存在依赖,则循环依赖
            for (let j in dependencies) {
                // 对依赖文件进行分析,将分析结果存入到图谱数组中
                graphArray.push(
                    moduleAnalyser(dependencies[j]) // 依赖的对象文件 
                ); // 推入graphArray中后,长度变长,下一个进行遍历分析的文件
            }
        }
    }

    // 关系图谱数据解构转化,将数组转化为对象
    const graph = {};
    graphArray.forEach(item => {
        graph[item.filename] = {
            dependencies: item.dependencies,
            code: item.code
        }
    });
    return graph;
}

// 最后的结果返回代码字符串
const generateCode = (entry) => {
    // 转化成字符串传入到闭包中
    const graph = JSON.stringify(makeDependenciedGraph(entry));
    // 代码在闭包中执行避免污染外部环境
    // require无法在浏览器中直接运行,需要进行创建require方法
    // export存储导出的内容 会转存到exports对象中
    // 返回一个字符串
    // localRequire转换文件的相对路径返回真实路径
    // 递归调用到最内层的依赖文件,然后通过exports导出
    return `
        (function(graph) {
            function require(module) {
                function localRequire(relativePath) {
                    return require(graph[module].dependencies[relativePath]);
                }
                var exports = {};
                (function(require, exports, code) {
                    eval(code);
                })(localRequire, exports, graph[module].code);
                return exports;
            };
            require('${entry}');
        })(${graph});
    `;
}

const code = generateCode('./src/index.js')
console.log(code)

// 注意:示例文件中本来是有注释的,但是注释加上去了之后就会出现问题,所以删除注释后在chrome://test模式下运行成功

webpack 管理多个项目

实现多个项目共用 node_modules 和 package.json

差异性不大的多个项目是否可以共用一个 node_modules ,使用公共组件部分、公共插件(抽npm)

方案
方案一

每个项目中独立配置项目差异 config 文件,相同配置使用根目录的 webpack.base.config.js

实现:

  • ./webpack.base.config.js、
  • projectA/webpack.config.js、
  • projectB/webpack.config.js
  • 根目录下的 package.jsonscripts 命令:npx webpack --config ./projectA/webpack.config.js && npx webpack --config ./projectB/webpack.config.js
方案二

使用根目录统一的 config 文件进行做差异化处理

实现:

  • ./webpack.config.js
  • 根目录下的 package.jsonscripts 命令:npx webpack --env PROJECT=projectA && npx webpack --env PROJECT=projectB
注意点

两种方案都使用了 webpack-merge 合并配置。

配置的主要问题是获取到具体项目的路径、以及定义配置的上下文环境 contextnode_modules 路径解析 resolve.modules

Demo项目地址

study webpack: https://github.com/Datura35422/study/tree/master/webpack-demos

pug demo: https://github.com/Datura35422/pug-demo/tree/master/website

博文收集

窥探原理:手写一个 JavaScript 打包器 #69