Alan

此刻想举重若轻,之前必要负重前行

手写简单的loader、Plugin、简单的webpack

手写简单的loader

目录

myLoader
 ├── loaders
 │   └── myLoader.js
 ├── package.json
 ├── src
 │   └── index.js
 └── webpack.config.js
console.log('hello webpack!');
const path =require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  module: {
    rules: [{
      test: /\.js$/,
      exclude: /node_modules/,
      use: [
        {
          loader: path.resolve(__dirname, './loaders/myLoader.js'),
          options: {
            key: 'my option value',
          },
        },
      ],
    }],
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
  },
};
module.exports = function (source) {
  return source.replace('webpack', this.query.key);
}

这个例子非常简单,就是通过自建的loader将项目中的webpack字符串替换成webpack中配置的字符串。myLoader.js中可以通过this.query接受webpack中配置的options参数。更多this上的属性参考(包括异步处理、回调…)

上面的例子通过打包后代码如下

console.log('hello my option value!')

webpack5中可以直接通过this.getOptions (schema)来获取options参数

webpack resolveLoader:

和之前提到的resolve的使用类似,就是用来偷懒的😂

使用resolveLoader改写后的wepack.config.js

const path =require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  resolveLoader: {
    modules: ['node_modules', './loaders'],
  },
  module: {
    rules: [{
      test: /\.js$/,
      exclude: /node_modules/,
      use: [
        {
          loader: 'myLoader',
          options: {
            key: 'my option',
          },
        },
      ],
    }],
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
  },
};

上面这个例子可以直接使用myLoader名,webpack会在node_modules./loaders中寻找对应的Loader。

自定义的loader中不要使用箭头函数,会产生this指向问题

手写简单的Plugin

目录

myPlugin
 ├── package.json
 ├── plugins
 │   └── date-webpack-plugin.js
 ├── src
 │   └── index.js
 └── webpack.config.js

complier提供了许多钩子,可以让我们在打包的不同时刻来进行不同的处理,这里使用了emit钩子

下面通过手写的plugin来实现在dist目录下生成一个author.txt文件

class DateWebpackPlugin {
  constructor(options) {
    // options是new插件时传进来的参数
    this.options = options;
  }

  apply(compiler) {
    const _this = this;
    compiler.hooks.emit.tapAsync('DateWebpackPlugin', (compilation, cb) => {
      compilation.assets['author.txt'] = {
        // 返回的资源
        source: function () {
          return `created by ${_this.options.author} ${new Date()}`;
        },
        // 最后生成的文件大小
        size: function () {
          return 19;
        }
      };
      // 由于emit是异步操作,所以最后要执行回调函数
      cb();
    })
  }
}

module.exports = DateWebpackPlugin;
const path = require('path');
const DateWebpackPlugin = require('./plugins/date-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
  },
  plugins: [
    new DateWebpackPlugin({
      author: 'Alan',
    }),
  ],
};

打包后会在dist目录下自动生成一个author.txt文件,内容如下

created by AlanSun Jun 07 2020 11:49:42 GMT+0800 (GMT+08:00)

react和vue脚手架webpack配置

  • create-react-app通过npm run eject暴露webpack配置
  • vue-cli通过vue.config.js配置webpack(可以通过configureWebpack自定义webpack配置)

手写一个简单的webpack打包工具

先提前安装以下需要的插件

npm i @babel/parser -D // 将js内容转化为抽象语法树
npm i @babel/traverse -D // 用来遍历抽象语法树
npm i @babel/core -D
npm i @babel/preset-env -D //es6->es5
npm i cli-highlight -D // 可选,命令行高亮插件

前置知识:

node:

项目目录:

bundler
 ├── bundler.js  // 主要文件
 └── src
     ├── course.js
     ├── index.js
     └── learn.js
import notify from './learn.js';

console.log(notify);
import { course } from './course.js';

const learnNotify = `time to learn ${course}`;

export default learnNotify;
export const course = 'webpack';

我将整个项目拆分成2个部分来分析

处理入口文件找到所有import文件

思路:

  1. 通过fs.readFileSync读取index.js的内容
  2. 使用@babel/parser将读取的内容转化为AST抽象语法树
  3. 使用@babel/traverse遍历找到所有import语句
  4. 分析出引用的文件,保存其路径

代码:

const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
// 命令行高亮工具
const highlight = require('cli-highlight').highlight

const moduleAnalysis = (filename) => {
  // 读取出index.js文件内容
  const content = fs.readFileSync(filename, 'utf-8');
  // 将文件内容转化为抽象语法树
  const ast = parser.parse(content, {
    sourceType: 'module'
  });
  // 遍历抽象语法树
  traverse(ast, {
    ImportDeclaration({ node }) {
      console.log(node);
    }
  })
  // console.log(highlight(ast));
  console.log(ast.program.body);
}
moduleAnalysis('./src/index.js');

通过parser转化成的抽象语法树

通过上图可以清楚看到我们现在要做的事情就是找到所有为type为importDeclaration的node属性

使用traverse得到的ImportDeclaration

最后对js文件进行babel处理,转化成浏览器能够识别的代码

完整代码

const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const path = require('path');
const babel = require('@babel/core');
// 命令行高亮工具
const highlight = require('cli-highlight').highlight

const moduleAnalysis = (filename) => {
  // 读取出index.js文件内容
  const content = fs.readFileSync(filename, 'utf-8');
  // 将文件内容转化为抽象语法树
  const ast = parser.parse(content, {
    sourceType: 'module'
  });
  const dependencies = {};
  // 遍历抽象语法树
  traverse(ast, {
    ImportDeclaration({ node }) {
      // 文件对应目录./src
      const dirPath = path.dirname(filename);
      // 绝对路径./src/learn.js(window操作系统)
      let filePath = ('./' + path.join(dirPath, node.source.value)).replace('\\', '/');
      dependencies[node.source.value] = filePath;
      // { './learn.js': './src/learn.js' }
      console.log(dependencies);
    }
  });
  // 转化成浏览器可以执行的代码
  const { code } = babel.transformFromAst(ast, null, {
    presets: ['@babel/preset-env']
  });
  console.log(highlight(code));
  return {
    filename,
    dependencies,
    code
  }
}
moduleAnalysis('./src/index.js');

通过入口文件分析出所有文件依赖

上面已经分析出了入口文件的一些依赖,接下来可以通过递归遍历来分析出所有的文件依赖并保存在变量中,先分析一些经过上面函数处理后的数据

{
  filename: './src/index.js',
  dependencies: { './learn.js': './src/learn.js' },
  code: '"use strict";\n' +
    '\n' +
    'var _learn = _interopRequireDefault(require("./learn.js"));\n' +
    '\n' +
    'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
    '\n' +
    'console.log(_learn["default"]);'
}

我们只需要遍历对象中的dependencies属性,把里面的路径名传入到上面的moduleAnalysis函数中,最终获取所有的依赖信息。

具体代码如下

const analysisDependenciesGraph = (entry) => {
  const entryModule = moduleAnalysis(entry);
  const graphList = [entryModule];
  for (let i = 0; i < graphList.length; i++) {
    const item = graphList[i];
    const { dependencies } = item;
    if (dependencies) {
      for (let i in dependencies) {
        graphList.push(moduleAnalysis(dependencies[i]))
      }
    }
  }
  const graph = {};
  graphList.forEach(({ filename, dependencies, code }) => {
    graph[filename] = {
      dependencies,
      code
    }
  });
  return graph;
}

分析出的所有依赖对象

{
  './src/index.js': {
    dependencies: { './learn.js': './src/learn.js' },
    code: '"use strict";\n' +
      '\n' +
      'var _learn = _interopRequireDefault(require("./learn.js"));\n' +
      '\n' +
      'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
      '\n' +
      'console.log(_learn["default"]);'
  },
  './src/learn.js': {
    dependencies: { './course.js': './src/course.js' },
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports["default"] = void 0;\n' +
      '\n' +
      'var _course = require("./course.js");\n' +
      '\n' +
      'var learnNotify = "time to learn ".concat(_course.course);\n' +
      'var _default = learnNotify;\n' +
      'exports["default"] = _default;'
  },
  './src/course.js': {
    dependencies: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports.course = void 0;\n' +
      "var course = 'webpack';\n" +
      'exports.course = course;'
  }
}

生成代码

const generateCode = (entry) => {
    const graph = JSON.stringify(analysisDependenciesGraph(entry));
    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});
    `;
}

生成后的代码就可以直接在浏览器运行了

image-20200613160248899

完整代码

const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const path = require('path');
const babel = require('@babel/core');
// 命令行高亮工具
const highlight = require('cli-highlight').highlight

const moduleAnalysis = (filename) => {
  // 读取出index.js文件内容
  const content = fs.readFileSync(filename, 'utf-8');
  // 将文件内容转化为抽象语法树
  const ast = parser.parse(content, {
    sourceType: 'module'
  });
  const dependencies = {};
  // 遍历抽象语法树
  traverse(ast, {
    ImportDeclaration({ node }) {
      // 文件对应目录./src
      const dirPath = path.dirname(filename);
      // 绝对路径./src/learn.js(window操作系统)
      let filePath = ('./' + path.join(dirPath, node.source.value)).replace('\\', '/');
      dependencies[node.source.value] = filePath;
      // { './learn.js': './src/learn.js' }
      console.log(dependencies);
    }
  });
  // 转化成浏览器可以执行的代码
  const { code } = babel.transformFromAst(ast, null, {
    presets: ['@babel/preset-env']
  });
  return {
    filename,
    dependencies,
    code
  }
}

const analysisDependenciesGraph = (entry) => {
  const entryModule = moduleAnalysis(entry);
  const graphList = [entryModule];
  for (let i = 0; i < graphList.length; i++) {
    const item = graphList[i];
    const { dependencies } = item;
    if (dependencies) {
      for (let j in dependencies) {
        graphList.push(moduleAnalysis(dependencies[j]))
      }
    }
  }
  const graph = {};
  graphList.forEach(({ filename, dependencies, code }) => {
    graph[filename] = {
      dependencies,
      code
    }
  });
  return graph;
}

const generateCode = (entry) => {
    const graph = JSON.stringify(analysisDependenciesGraph(entry));
    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(highlight(code));

总结

到这里总算是对webpack有了大体的了解了。奈何当我学完webpack后看到了vite这个东西😒。。。

image-20200615160241265

评论