webpack深入 - 图1

一、create-react-app react-project

本部分只讲述通过脚手架创建的项目的分析路线及步骤,具体每个文件夹里面讲述了什么内容分别在源码中进行注释讲解。

通过 npm run eject将配置文件暴露出来

  1. config—>paths.js(向外暴露出路径)
  2. scripts—>start.js(开发环境对应的文件)
  3. webpack.config.js(主要内容为对loader和plugin的配置,将来自己修改的时候可以直接在这个文件夹里面进行loader和plugin的修改)(核心)
  4. scripts—>build.js(生产环境对应的文件,与开发环境对应的文件差不多)
  5. 在package.json的scirpts命令中加入cross-env ENV-NAME=VALUE 指定运行时的环境变量

二、vue create vue-project

这里只讲述通过脚手架创建的项目的分析路线及步骤,具体每个文件夹里面讲述了什么内容分别在源码中进行注释。

  • 通过vue inspect --mode=development > webpack.dev.js将vue开发环境配置打包一起放在webpack.dev.js文件下面,开发环境代码只需要研究webpack.dev.js文件即可
  • 通过vue inspect --mode=production > webpack.prod.js将vue生产环境配置打包一起放在webpack.prod.js文件下面,生产环境代码只需要研究webpack.prod.js文件即可

开发环境文件webpack.dev.js 生产环境文件webpack.prod.js(除了在css上面以及多线程打包上面进行了一些修改,其余和开发环境是一样的)

三、自定义loader

3.1预备知识

loader本质上是一个函数

  1. loader的执行顺序在use数组里面是从下往上执行
  2. loader里面有一个pitch方法,use数组中pitch方法的执行顺序是从上往下执行,因此我们如果想先执行某些功能,可以先在pitch方法中定义
  3. 同步loader
  1. // 方式一
  2. module.exports = function (content, map, meta) {
  3. console.log(111);
  4. return content;
  5. }
  6. // 方式二
  7. module.exports = function (content, map, meta) {
  8. console.log(111);
  9. this.callback(null, content, map, meta);
  10. }
  11. module.exports.pitch = function () {
  12. console.log('pitch 111');
  13. }
  1. 异步loader
  1. // 异步loader(推荐使用,loader在异步加载的过程中可以执行其余的步骤)
  2. module.exports = function (content, map, meta) {
  3. console.log(222);
  4. const callback = this.async();
  5. setTimeout(() => {
  6. callback(null, content);
  7. }, 1000)
  8. }
  9. module.exports.pitch = function () {
  10. console.log('pitch 222');
  11. }
  1. 获取options库:

安装loader-utils:cnpm install loader-utils 在loader中引入并使用 6. 校验options库: 在loader中从schema-utils引入validate并使用 创建schema.json文件校验规则并引入使用

loader3.js中代码

  1. // 1.1 获取options 引入
  2. const {
  3. getOptions
  4. } = require('loader-utils');
  5. // 2.1 获取validate(校验options是否合法)引入
  6. const {
  7. validate
  8. } = require('schema-utils');
  9. // 2.3创建schema.json文件校验规则并引入使用
  10. const schema = require('./schema');
  11. module.exports = function(content, map, meta) {
  12. // 1.2 获取options 使用
  13. const options = getOptions(this);
  14. console.log(333, options);
  15. // 2.2校验options是否合法 使用
  16. validate(schema, options, {
  17. name: 'loader3'
  18. })
  19. return content;
  20. }
  21. module.exports.pitch = function() {
  22. console.log('pitch 333');
  23. }

schema.json中代码

  1. {
  2. "type": "object",
  3. "properties": {
  4. "name": {
  5. "type": "string",
  6. "description": "名称~"
  7. }
  8. },
  9. "additionalProperties": false // 如果设置为true表示除了校验前面写的string类型还可以 接着 校验其余类型,如果为false表示校验了string类型之后不可以再校验其余类型
  10. }

webpack.config.js中代码

  1. const path = require('path');
  2. module.exports = {
  3. module: {
  4. rules: [{
  5. test: /\.js$/,
  6. use: [
  7. {
  8. loader: 'loader3',
  9. // options部分
  10. options: {
  11. name: 'jack',
  12. age: 18
  13. }
  14. }
  15. ]
  16. }]
  17. },
  18. // 配置loader解析规则:我们的loader去哪个文件夹下面寻找(这里表示的是同级目录的loaders文件夹下面寻找)
  19. resolveLoader: {
  20. modules: [
  21. 'node_modules',
  22. path.resolve(__dirname, 'loaders')
  23. ]
  24. }
  25. }
  26. 复制代码

3.2自定义babel-loader

  1. 创建校验规则

babelSchema.json

  1. {
  2. "type": "object",
  3. "properties": {
  4. "presets": {
  5. "type": "array"
  6. }
  7. },
  8. "addtionalProperties": true
  9. }
  10. 复制代码
  1. 创建loader

babelLoader.js

  1. const { getOptions } = require('loader-utils');
  2. const { validate } = require('schema-utils');
  3. const babel = require('@babel/core');
  4. const util = require('util');
  5. const babelSchema = require('./babelSchema.json');
  6. // babel.transform用来编译代码的方法
  7. // 是一个普通异步方法
  8. // util.promisify将普通异步方法转化成基于promise的异步方法
  9. const transform = util.promisify(babel.transform);
  10. module.exports = function (content, map, meta) {
  11. // 获取loader的options配置
  12. const options = getOptions(this) || {};
  13. // 校验babel的options的配置
  14. validate(babelSchema, options, {
  15. name: 'Babel Loader'
  16. });
  17. // 创建异步
  18. const callback = this.async();
  19. // 使用babel编译代码
  20. transform(content, options)
  21. .then(({code, map}) => callback(null, code, map, meta))
  22. .catch((e) => callback(e))
  23. }
  1. babelLoader使用

webpack.config.js

  1. const path = require('path');
  2. module.exports = {
  3. module: {
  4. rules: [{
  5. test: /\.js$/,
  6. loader: 'babelLoader',
  7. options: {
  8. presets: [
  9. '@babel/preset-env'
  10. ]
  11. }
  12. }]
  13. },
  14. // 配置loader解析规则:我们的loader去哪个文件夹下面寻找(这里表示的是同级目录的loaders文件夹下面寻找)
  15. resolveLoader: {
  16. modules: [
  17. 'node_modules',
  18. path.resolve(__dirname, 'loaders')
  19. ]
  20. }
  21. }

四、自定义plugin

4.1 预备知识-compiler钩子

4.1.2 tapable

hooks tapable

  1. 安装tapable:npm install tapable -D
  2. 初始化hooks容器 2.1 同步hooks,任务会依次执行:SyncHook、SyncBailHook 2.2 异步hooks,异步并行:AsyncParallelHook,异步串行:AsyncSeriesHook
  3. 往hooks容器中注册事件/添加回调函数
  4. 触发hooks
  5. 启动文件:node tapable.test.js

文件tapable.test.js

  1. const { SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook } = require('tapable');
  2. class Lesson {
  3. constructor() {
  4. // 初始化hooks容器
  5. this.hooks = {
  6. // 同步hooks,任务会依次执行
  7. // go: new SyncHook(['address'])
  8. // SyncBailHook:一旦有返回值就会退出~
  9. go: new SyncBailHook(['address']),
  10. // 异步hooks
  11. // AsyncParallelHook:异步并行
  12. // leave: new AsyncParallelHook(['name', 'age']),
  13. // AsyncSeriesHook: 异步串行
  14. leave: new AsyncSeriesHook(['name', 'age'])
  15. }
  16. }
  17. tap() {
  18. // 往hooks容器中注册事件/添加回调函数
  19. this.hooks.go.tap('class0318', (address) => {
  20. console.log('class0318', address);
  21. return 111;
  22. })
  23. this.hooks.go.tap('class0410', (address) => {
  24. console.log('class0410', address);
  25. })
  26. // tapAsync常用,有回调函数
  27. this.hooks.leave.tapAsync('class0510', (name, age, cb) => {
  28. setTimeout(() => {
  29. console.log('class0510', name, age);
  30. cb();
  31. }, 2000)
  32. })
  33. // 需要返回promise
  34. this.hooks.leave.tapPromise('class0610', (name, age) => {
  35. return new Promise((resolve) => {
  36. setTimeout(() => {
  37. console.log('class0610', name, age);
  38. resolve();
  39. }, 1000)
  40. })
  41. })
  42. }
  43. start() {
  44. // 触发hooks
  45. this.hooks.go.call('c318');
  46. this.hooks.leave.callAsync('jack', 18, function () {
  47. // 代表所有leave容器中的函数触发完了,才触发
  48. console.log('end~~~');
  49. });
  50. }
  51. }
  52. const l = new Lesson();
  53. l.tap();
  54. l.start();
  55. 复制代码

4.1.2 compiler钩子

  1. 工作方式:异步串行执行,因此下面代码输出顺序如下: 1.1 emit.tap 111 1.2 1秒后输出 emit.tapAsync 111 1.3 1秒后输出 emit.tapPromise 111 1.4 afterEmit.tap 111 1.5 done.tap 111
  2. tapAsync和tapPromise表示异步
  3. 这边只简单介绍了几个complier,具体开发的过程中可以根据文档介绍编写(很方便的)
  1. class Plugin1 {
  2. apply(complier) {
  3. complier.hooks.emit.tap('Plugin1', (compilation) => {
  4. console.log('emit.tap 111');
  5. })
  6. complier.hooks.emit.tapAsync('Plugin1', (compilation, cb) => {
  7. setTimeout(() => {
  8. console.log('emit.tapAsync 111');
  9. cb();
  10. }, 1000)
  11. })
  12. complier.hooks.emit.tapPromise('Plugin1', (compilation) => {
  13. return new Promise((resolve) => {
  14. setTimeout(() => {
  15. console.log('emit.tapPromise 111');
  16. resolve();
  17. }, 1000)
  18. })
  19. })
  20. complier.hooks.afterEmit.tap('Plugin1', (compilation) => {
  21. console.log('afterEmit.tap 111');
  22. })
  23. complier.hooks.done.tap('Plugin1', (stats) => {
  24. console.log('done.tap 111');
  25. })
  26. }
  27. }
  28. module.exports = Plugin1;

4.2 预备知识-compilation钩子

4.2.1 小插曲:nodejs环境中调试

  1. package.json中输入(—inspect-brk 表示通过断点的方式调试,,,,,,./node_modules/webpack/bin/webpack.js” 表示调试这个文件,,,,,,node 表示通过node运行)
  1. "scripts": {
  2. "start": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
  3. }
  4. 复制代码
  1. 在需要调试的地方打一个debugger
  2. 通过node运行文件
  3. 在一个网站中右击检查,点击绿色图标

webpack深入 - 图2 便可以调试了,和在网页中调试代码一样的 webpack深入 - 图3

4.2.2 compilation钩子

  1. 初始化compilation钩子
  2. 往要输出资源中,添加一个a.txt文件
  3. 读取b.txt中的内容,将b.txt中的内容添加到输出资源中的b.txt文件中 3.1 读取b.txt中的内容需要使用node的readFile模块 3.2 将b.txt中的内容添加到输出资源中的b.txt文件中除了使用 2 中的方法外,还有两种形式可以使用 3.2.1 借助RawSource 3.2.2 借助RawSource和emitAsset
  1. const fs = require('fs');
  2. const util = require('util');
  3. const path = require('path');
  4. const webpack = require('webpack');
  5. const { RawSource } = webpack.sources;
  6. // 将fs.readFile方法变成基于promise风格的异步方法
  7. const readFile = util.promisify(fs.readFile);
  8. /*
  9. 1. 初始化compilation钩子
  10. 2. 往要输出资源中,添加一个a.txt文件
  11. 3. 读取b.txt中的内容,将b.txt中的内容添加到输出资源中的b.txt文件中
  12. 3.1 读取b.txt中的内容需要使用node的readFile模块
  13. 3.2 将b.txt中的内容添加到输出资源中的b.txt文件中除了使用 2 中的方法外,还有两种形式可以使用
  14. 3.2.1 借助RawSource
  15. 3.2.2 借助RawSource和emitAsset
  16. */
  17. class Plugin2 {
  18. apply(compiler) {
  19. // 1.初始化compilation钩子
  20. compiler.hooks.thisCompilation.tap('Plugin2', (compilation) => {
  21. // debugger
  22. // console.log(compilation);
  23. // 添加资源
  24. compilation.hooks.additionalAssets.tapAsync('Plugin2', async (cb) => {
  25. // debugger
  26. // console.log(compilation);
  27. const content = 'hello plugin2';
  28. // 2.往要输出资源中,添加一个a.txt
  29. compilation.assets['a.txt'] = {
  30. // 文件大小
  31. size() {
  32. return content.length;
  33. },
  34. // 文件内容
  35. source() {
  36. return content;
  37. }
  38. }
  39. const data = await readFile(path.resolve(__dirname, 'b.txt'));
  40. // 3.2.1 compilation.assets['b.txt'] = new RawSource(data);
  41. // 3.2.1
  42. compilation.emitAsset('b.txt', new RawSource(data));
  43. cb();
  44. })
  45. })
  46. }
  47. }
  48. module.exports = Plugin2;
  49. 复制代码

自定义CopyWebpackPlugin

CopyWebpackPlugin的功能:将public文件夹中的文件复制到dist文件夹下面(忽略index.html文件)

  1. 创建schema.json校验文件
{
  "type": "object",
  "properties": {
    "from": {
      "type": "string"
    },
    "to": {
      "type": "string"
    },
    "ignore": {
      "type": "array"
    }
  },
  "additionalProperties": false
}
  1. 创建CopyWebpackPlugin.js插件文件

编码思路 下载schema-utils和globby:npm install globby schema-utils -D 将from中的资源复制到to中,输出出去 1. 过滤掉ignore的文件 2. 读取paths中所有资源 3. 生成webpack格式的资源 4. 添加compilation中,输出出去

const path = require('path');
const fs = require('fs');
const {promisify} = require('util')

const { validate } = require('schema-utils');
const globby = require('globby');// globby用来匹配文件目标
const webpack = require('webpack');

const schema = require('./schema.json');
const { Compilation } = require('webpack');

const readFile = promisify(fs.readFile);
const {RawSource} = webpack.sources

class CopyWebpackPlugin {
  constructor(options = {}) {
    // 验证options是否符合规范
    validate(schema, options, {
      name: 'CopyWebpackPlugin'
    })

    this.options = options;
  }
  apply(compiler) {
    // 初始化compilation
    compiler.hooks.thisCompilation.tap('CopyWebpackPlugin', (compilation) => {
      // 添加资源的hooks
      compilation.hooks.additionalAssets.tapAsync('CopyWebpackPlugin', async (cb) => {
        // 将from中的资源复制到to中,输出出去
        const { from, ignore } = this.options;
        const to = this.options.to ? this.options.to : '.';

        // context就是webpack配置
        // 运行指令的目录
        const context = compiler.options.context; // process.cwd()
        // 将输入路径变成绝对路径
        const absoluteFrom = path.isAbsolute(from) ? from : path.resolve(context, from);

        // 1. 过滤掉ignore的文件
        // globby(要处理的文件夹,options)
        const paths = await globby(absoluteFrom, { ignore });

        console.log(paths); // 所有要加载的文件路径数组

        // 2. 读取paths中所有资源
        const files = await Promise.all(
          paths.map(async (absolutePath) => {
            // 读取文件
            const data = await readFile(absolutePath);
            // basename得到最后的文件名称
            const relativePath = path.basename(absolutePath);
            // 和to属性结合
            // 没有to --> reset.css
            // 有to --> css/reset.css(对应webpack.config.js中CopyWebpackPlugin插件的to的名称css)
            const filename = path.join(to, relativePath);

            return {
              // 文件数据
              data,
              // 文件名称
              filename
            }
          })
        )

        // 3. 生成webpack格式的资源
        const assets = files.map((file) => {
          const source = new RawSource(file.data);
          return {
            source,
            filename: file.filename
          }
        })

        // 4. 添加compilation中,输出出去
        assets.forEach((asset) => {
          compilation.emitAsset(asset.filename, asset.source);
        })

        cb();
      })
    })
  }

}

module.exports = CopyWebpackPlugin;
  1. 在webpack.config.js中使用

五、自定义Webpack

5.1 Webpack 执行流程

  1. 初始化 Compiler:webpack(config) 得到 Compiler 对象
  2. 开始编译:调用 Compiler 对象 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件。
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行编译,再找出该模块依赖的模块,递归直到所有模块被加载进来
  5. 完成模块编译: 在经过第 4 步使用 Loader 编译完所有模块后,得到了每个模块被编译后的最终内容以及它们之间的依赖关系。
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表。(注意:这步是可以修改输出内容的最后机会)
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

5.2 准备工作

  1. 创建文件夹myWebpack
  2. 创建src—>(add.js / count.js / index.js),写入对应的js代码
  3. 创建config—>webpack.config.js写入webpack基础配置(entry和output)
  4. 创建lib文件夹,里面写webpack的主要配置
  5. 创建script—>build.js(将lib文件夹下面的myWebpack核心代码和config文件下的webpack基础配置引入并调用run()函数开始打包)
  6. 为了方便启动,控制台通过输入命令 npm init -y拉取出package.json文件,修改文件中scripts部分为"build": "node ./script/build.js"表示通过在终端输入命令npm run build时会运行/script/build.js文件,在scripts中添加"debug": "node --inspect-brk ./script/build.js"表示通过在终端输入命令npm run debug时会调试/script/build.js文件中的代码,调试代码的步骤第四章已经介绍

5.3 使用babel解析文件

  1. 创建文件lib—>myWebpack1—>index.js
  2. 下载三个babel包 babel官网

npm install @babel/parser -D用来将代码解析成ast抽象语法树 npm install @babel/traverse -D用来遍历ast抽象语法树代码 npm install @babel/core-D用来将代码中浏览器不能识别的语法进行编译 3. 编码思路 1. 读取入口文件内容 2. 将其解析成ast抽象语法树 3. 收集依赖 4. 编译代码:将代码中浏览器不能识别的语法进行编译

index.js

const fs = require('fs');
const path = require('path');

// babel的库
const babelParser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');

function myWebpack(config) {
  return new Compiler(config);
}

class Compiler {
  constructor(options = {}) {
    this.options = options;
  }
  // 启动webpack打包
  run() {
    // 1. 读取入口文件内容
    // 入口文件路径
    const filePath = this.options.entry;
    const file = fs.readFileSync(filePath, 'utf-8');
    // 2. 将其解析成ast抽象语法树
    const ast = babelParser.parse(file, {
      sourceType: 'module' // 解析文件的模块化方案是 ES Module
    })
    // debugger;
    console.log(ast);

    // 获取到文件文件夹路径
    const dirname = path.dirname(filePath);

    // 定义存储依赖的容器
    const deps = {}

    // 3. 收集依赖
    traverse(ast, {
      // 内部会遍历ast中program.body,判断里面语句类型
      // 如果 type:ImportDeclaration 就会触发当前函数
      ImportDeclaration({node}) {
        // 文件相对路径:'./add.js'
        const relativePath = node.source.value;
        // 生成基于入口文件的绝对路径
        const absolutePath = path.resolve(dirname, relativePath);
        // 添加依赖
        deps[relativePath] = absolutePath;
      }
    })

    console.log(deps);

    // 4. 编译代码:将代码中浏览器不能识别的语法进行编译
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']
    })

    console.log(code);
  }
}

module.exports = myWebpack;

5.4 模块化

我们开发代码过程中讲究的是模块化开发,不同功能的代码放在不同的文件中 创建myWebpack2—>parser.js(放入解析代码)/Compiler.js(放入编译代码)/index.js(主文件)

5.5 收集所有的依赖

所有代码位于myWebpack文件夹中 Compiler.js文件中build函数用于构建代码,run函数中modules通过递归遍历收集所有的依赖,depsGraph用于将依赖整理更好依赖关系图(具体的代码功能都在代码中进行了注释)

5.6 生成打包之后的bundle

代码位于myWebpack—>Compiler.js中的bundle部分 整个myWebpack—>Compiler.js代码

const path = require('path');
const fs = require('fs');
const {
  getAst,
  getDeps,
  getCode
} = require('./parser')

class Compiler {
  constructor(options = {}) {
      // webpack配置对象
      this.options = options;
      // 所有依赖的容器
      this.modules = [];
    }
    // 启动webpack打包
  run() {
    // 入口文件路径
    const filePath = this.options.entry;

    // 第一次构建,得到入口文件的信息
    const fileInfo = this.build(filePath);

    this.modules.push(fileInfo);

    // 遍历所有的依赖
    this.modules.forEach((fileInfo) => {
      /**
       {
          './add.js': '/Users/xiongjian/Desktop/atguigu/code/05.myWebpack/src/add.js',
          './count.js': '/Users/xiongjian/Desktop/atguigu/code/05.myWebpack/src/count.js'
        } 
       */
      // 取出当前文件的所有依赖
      const deps = fileInfo.deps;
      // 遍历
      for (const relativePath in deps) {
        // 依赖文件的绝对路径
        const absolutePath = deps[relativePath];
        // 对依赖文件进行处理
        const fileInfo = this.build(absolutePath);
        // 将处理后的结果添加modules中,后面遍历就会遍历它了~(递归遍历)
        this.modules.push(fileInfo);
      }

    })

    console.log(this.modules);

    // 将依赖整理更好依赖关系图
    /*
      {
        'index.js': {
          code: 'xxx',
          deps: { 'add.js': "xxx" }
        },
        'add.js': {
          code: 'xxx',
          deps: {}
        }
      }
    */
    const depsGraph = this.modules.reduce((graph, module) => {
      return {
        ...graph,
        [module.filePath]: {
          code: module.code,
          deps: module.deps
        }
      }
    }, {})

    console.log(depsGraph);

    this.generate(depsGraph)

  }

  // 开始构建
  build(filePath) {
    // 1. 将文件解析成ast
    const ast = getAst(filePath);
    // 2. 获取ast中所有的依赖
    const deps = getDeps(ast, filePath);
    // 3. 将ast解析成code
    const code = getCode(ast);

    return {
      // 文件路径
      filePath,
      // 当前文件的所有依赖
      deps,
      // 当前文件解析后的代码
      code
    }
  }

  // 生成输出资源
  generate(depsGraph) {

    /* index.js的代码
      "use strict";\n' +
      '\n' +
      'var _add = _interopRequireDefault(require("./add.js"));\n' +
      '\n' +
      'var _count = _interopRequireDefault(require("./count.js"));\n' +
      '\n' +
      'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
      '\n' +
      'console.log((0, _add["default"])(1, 2));\n' +
      'console.log((0, _count["default"])(3, 1));
    */

    const bundle = `
      (function (depsGraph) {
        // require目的:为了加载入口文件
        function require(module) {
          // 定义模块内部的require函数
          function localRequire(relativePath) {
            // 为了找到要引入模块的绝对路径,通过require加载
            return require(depsGraph[module].deps[relativePath]);
          }
          // 定义暴露对象(将来我们模块要暴露的内容)
          var exports = {};

          (function (require, exports, code) {
            eval(code);
          })(localRequire, exports, depsGraph[module].code);

          // 作为require函数的返回值返回出去
          // 后面的require函数能得到暴露的内容
          return exports;
        }
        // 加载入口文件
        require('${this.options.entry}');

      })(${JSON.stringify(depsGraph)})
    `
      // 生成输出文件的绝对路径
    const filePath = path.resolve(this.options.output.path, this.options.output.filename)
      // 写入文件
    fs.writeFileSync(filePath, bundle, 'utf-8');
  }
}

module.exports = Compiler;