likes
comments
collection
share

【初学者笔记】前端工程化必须要掌握的 webpack

作者站长头像
站长
· 阅读数 17

安装

现在学习的是 webpack4 的最新版,新建文件夹,npm init -y 初始化一下,然后执行下面命令进行安装,需要同时安装 webpackwebpack-cli

npm install webpack@4.43.0 webpack-cli@3.3.12 -D

不建议全局安装 webpack, 因为全局安装会锁定版本,多个项目之间可能依赖的 webpack 版本不同, 所以还是建议项目内安装。

可以根目录下新建 .npmrc 文件,指定当前项目安装依赖的源地址

registry=https://registry.npm.taobao.org

webpack 的零配置

webpack 宣传的一个噱头就是零配置构建, 也就是什么配置都不写就可以进行打包,虽然确实是可以执行,但其实是满足不了实际开发需要的。

先来随便跑一下,新建 src/index.js 文件。我们在 index.js 中 只写一行

console.log("hello, webpack !")

然后执行

npx webpack -v

npx 在当前项目下查找并启动 webpack 。可以看到根目录下生成 dist/main.js 文件。

建议在 package.json 文件中,添加构建命令,便于添加构建参数,也更加易读。

  "scripts": {
    "dev": "webpack"
  },

【初学者笔记】前端工程化必须要掌握的 webpack

虽然我只写了一行代码,但是打包出来却有这么多字符,看下面的进度条就知道后面还有很多了。

是因为 webpack 默认给做了很多兼容,这也是相对于其他构建工具,出现了很多的冗余代码。

但其实问题不大, webpack5 进行了一些优化,但是当模块变的越来越多时候,就没有什么太大改变了。

构建信息

看一下构建时都输出了什么信息

【初学者笔记】前端工程化必须要掌握的 webpack

打包出来的文件内容有很多,但是主结构很简单,其实就是一个自执行函数,如下

(function(modules) {
// todo
})({
  "./src/login.js": (function(module, exports) {
    eval("xxxxx")
  })
})

在这个结构中,可以看到自执行函数的参数是一个对象,对象的每个 keyvalue 都包含一个 chunk,也就是 代码片段

也可以包含多个 chunk , 被称为 chunkschunk组 ,还可以被称为 chunk Names

这一整个对象中可以包含多个 key:value,被称为依赖图谱。

构建后产生的资源文件, 称呼为 bundle 文件 。

几者遵循以下关系:

  • 一个 bundle 对应至少一个 chunks
  • 一个 chunks 对应至少一个 module
  • 一个 module 对应至少一个 chunk

一句话总结:

modulechunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字:

我们直接写出来的是 modulewebpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle

webpack的配置文件

如果没有添加配置文件,会走它的默认配置,也就是所谓的零配置,如果添加了配置文件 webpack 会按照配置文件里的配置进行打包。

通常情况下 webpack 的配置文件的文件名叫做 webpack.config.js ,其实也可以通过配置修改成别的,但是基本上没什么必要。

修改 package.json 文件,在 script 中 新建命令即可

--config 指定配置文件

 "warbler": "webpack --config ./warbler.config.js"

由于配置文件的内容是非常多的,语法也很难一时全记下来,所以我们可以通过一点点小技巧,就可以让配置文件具有语法提示。

首先安装 @types/webpack

npm i -D @types/webpack

然后再配置文件中注明类型就可以了。

// webpack.config.js
/**
 * @type {import('webpack').Configuration}
 */
module.exports = {
}

babel.config.js 也是支持同样方式的。

npm i -D @types/babel__core
/**
 * @type {import('@babel/core').TransformOptions}
 */

vue.config.js 也是可以的,且不用安装其他包。

/**
 * @type {import('@vue/cli-service').ProjectOptions}
 */

webpack的配置参数

webpack 是基于 nodejs 的, 所有的 nodejs 核心模块的 api 都可以直接引入使用。

entry

执行打包任务的入口,默认是 src/index ,支持相对路径, 也支持绝对路径。

entry: "./src/index.js"

支持 stringarrobject 如果传入的是字符串,也会在构建期间被转换成对象结构,上面的代码和下面的代码结果是一样的。

  entry: {
    main: "./src/index.js"
  },

支持 spa(单入口) 和 mpa(多入口)。多入口会输出多个 bundle 文件。

  entry: {
    index: "./src/index.js",
    login: "./src/login.js",
    home: "./src/home.js"
  },

output

输出资源文件的信息 , 包括 存储位置文件名称

  • path : 存储位置,打包出来的文件放在哪里 默认是 dist 要求是绝对路径;
  • filename : 文件名称,打包出来的文件叫什么。
  output: {
    path: path.resolve(__dirname, './dist'), 
    filename: "main.js"
  },

但是如果 entry 设置了多入口的话,就会产生多个 main.js 这样就会报错。

【初学者笔记】前端工程化必须要掌握的 webpack

解决方法就是使用 占位符,语法如下

filename: "[name].js"

作用就是 entry 中的 key 叫什么,打包后的 filename 就叫什么,多个入口就会对应多个出口了。

mode

打包模式,默认是生产模式,生产模式下会做代码压缩,摇树等操作。

  • development 开发模式
  • production 生产模式
  • none 默认

plugins

插件,插件的本质就是一个类。后面会单独讲解各种常用插件的配置。

plugins 的执行顺序是从上到下的。

plugins:[]

module

模块,webpack 默认只支持 .js.json 文件,像我们平时常用的 .vue.png.scss.css.ts 等都是不支持的。

所以如果想要 webpack 支持其他类型的文件,就需要不同类型的 loader 进行解析。

下面这个配置的意思就是,当检测(test)到 .css 后缀文件的时候,使用(use) 什么 loader 来进行处理。

// webpack.config.js
 module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"]
      },
    ]
  }
  • css-loader :把 css 模块序列化,让 webpack 认识 css 语法并可以生成 chunk
  • style-loader :把 css 整合在 html 文件的 head 标签的 style 中。

当多个 loader 作用于 一个模块的时候, 是按照自右向左的顺序执行的,也就是先执行 css-loader 在执行 style-loader

其实一个 loader 也可以搞定上面两件事,但是建议一个 loader 只做一件事情,也就是遵循 单一职责 原则。

也可以给 loader 书写配置,这个时候就要用对象的结构了。

// webpack.config.js
 module: {
    rules: [
      {
        test: /\.less$/,
        use: [
          {
            loader: 'style-loader',
          },
          {
            loader: "css-loader",
            options: {
              modules: true
            }
          },
          {
            loader: 'postcss-loader',
          },
          {
            loader: 'less-loader',
          }
        ]
      }
    ]
  }
  • less-loader :把 less 语法 转化为 css 语法。
  • postcss-loaderpostcss 是一个 工具集 ,这个就十分强大了,postcsscss 的意义,就等同于 babel 对于 js 的意义,他自身作为插件,还可以携带插件。

resolveLoader

解析 loader, 告知 webpack 如何匹配 loader。当我们自定义一些 loader 的时候。可以通过绝对路径的方式导入,但是过于繁琐,当我们通过这个字段指定文件夹的时候,就可以像使用第三方 loader 一样直接写 loader 的名字了。

// webpack.config.js
 resolveLoader: {
    // 默认是 node_modules
    modules: ["node_modules", "./myLoaders"]
  },

常用插件的常用配置

html-webpack-plugin

版本:4.5.2

  • template:使用指定模板生成 html
  • filename:打包后的 html 文件名。
  • chunks:打包后的 html 会包含哪些 chunk
// webpack.config.js
  plugins: [
    // 自动生成 html 文件 , 引入 bundle 文件, 压缩 html
    new htmlWebpackPlugin({
      // 模板匹配
      template: "./src/index.html",
      filename: "index.html",
      chunks: ["index", 'login'],
    }),
  ]

如果要生成多个 html,只需要创建多个实例即可。

// webpack.config.js
plugins: [
    // 自动生成html 文件 ,引入bundle文件,压缩html
    new htmlWebpackPlugin({
      // 模板匹配
      template: "./src/index.html",
      filename: "index.html",
      chunks: ["index", 'login'],
    }),
    // 自动生成html 文件 ,引入bundle文件,压缩html
    new htmlWebpackPlugin({
      // 模板匹配
      template: "./src/index.html",
      filename: "login.html",
      chunks: ["login"],
    }),
    // 自动生成html 文件 ,引入bundle文件,压缩html
    new htmlWebpackPlugin({
      // 模板匹配
      template: "./src/index.html",
      filename: "home.html",
      chunks: ["home"],
    }),
  ]

但是作为能懒则懒得程序员,是万万不可能写这种代码的,因为 webpack 的配置文件本身就是一个对象。所以我们当然可以利用一些方法来自动生成配置项。

首先要规定好结构,本例中每个文件夹中存放一个 index.htmlindex.js ,结构如下。当然可以自己随意定义结构,然后自己编写对应的函数体就可以了。

├── src 
     ├── list 
     │   ├── index.html    
     │   └── index.js
     ├── login 
     │   ├── index.html    
     │   └── index.js
     ├── detail 
     │   ├── index.html    
     │   └── index.js
     ├── index 
     │   ├── index.html    
     │   └── index.js

配置如下,利用 setMap 方法自动生成 entryhtmlWebpackPlugins,这样每次添加文件都不用我们修改配置了。

// webpack.config.js
const path = require('path')
const htmlWebpackPlugin = require("html-webpack-plugin")
// 模糊匹配路径
const glob = require('glob')

// 自动生成 entry 和 htmlWebpackPlugins
const setMap = () => {
  const entry = {};
  const htmlWebpackPlugins = []
  // 模糊匹配 src 目录下 任意目录下的 index.js 返回的是文件的绝对路径
  const entryFiles = glob.sync(path.join(__dirname, "./src/*/index.js"))
  // 遍历匹配到的结果
  entryFiles.forEach((entryFile) => {
    // 获取到文件名
    const pageName = entryFile.match(/src\/(.*)\/index\.js$/)[1]
    // 生成 entry
    entry[pageName] = entryFile
    // 生成 htmlWebpackPlugins
    htmlWebpackPlugins.push(
      new htmlWebpackPlugin({
        template: `./src/${pageName}/index.html`,
        filename: `${pageName}.html`,
        chunks: [pageName]
      }))
  })

  return {
    entry,
    htmlWebpackPlugins
  }
}

const { entry, htmlWebpackPlugins } = setMap()

// 配置文件
module.exports = {
  entry,
  // 输出资源文件的信息
  output: {
    // 存储位置
    path: path.resolve(__dirname, './dist'),
    // 文件名称
    filename: " [name].js"
  },
  // 打包模式
  mode: "development",
  // 插件
  plugins: [
    ...htmlWebpackPlugins,
  ],
}

postcss

postcss 也有他自己的配置文件,根目录下新建 postcss.config.js ,也是个模块,通过require("autoprefixer") 安装插件。

// postcss.config.js
module.exports = {
  plugins: [
    require("autoprefixer"),
    require("cssnano")
  ]
}

postcss的插件autoprefixer

autoprefixer 自动给 css 属性加前缀,使用 can i use 的数据。

但是这样还不会生效,需要设置 browserslist目标浏览器集合 ,也就是 需要兼容的浏览器版本,用到浏览器集合的工具会根据 browserslist 的描述, 针对性的输出兼容性的代码。

browserslist 的设置有两种方式。

你可以直接在 package.json 文件中添加属性如下

  "browserslist": [
    ">1%",
    "last 2 versions"
  ]

也可以根目录下新建 .browserslistrc 文件,直接写,什么括号都不用。

// .browserslistrc
>1%
last 2 versions
  • >1% : 全球市场占有率 大于1%的浏览器。
  • last 2 versions : 兼容浏览器的最近两个大版本。

可以使用 npx browserslist 查询兼容的浏览器版本。

postcss的插件cssnano

压缩 css 代码。

mini-css-extract-plugin

css 代码抽离出来单独放到一个文件里,可以指定文件名,支持绝对路径,会自动生成文件夹。需要在写 loader的位置, 使用 minicss.loader 替换 style-loader

// webpack.config.js
const minicss = require("mini-css-extract-plugin")
module.exports = {
  plugins: [
    new minicss({
      filename: 'style/index.css'
    })
  ],
  module: {
    rules: [
       {
        test: /\.less$/,
        use: [
          minicss.loader,
          {
            loader: "css-loader",
            options: {
              modules: true
            }
          },
          {
            loader: 'postcss-loader',
          },
          {
            loader: 'less-loader',
          }
        ]
      }
    ]
  }
}

clean-webpack-plugin

每次打包之前清理打包目录。

// webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
  plugins: [
     new CleanWebpackPlugin()
  ],
}

实现自定义loader

loader的引用

根目录下新建 myLoaders/a-loader.js 文件,然后在 module 中需要使用绝对路径的方式引入。

// webpack.config.js
module: {
    rules: [
      {
        test: /\.js$/,
        use: [
         {
            loader: path.resolve(__dirname, './myLoaders/a-loader.js'),
          },
        ]
      },
    ]
  }

loader的结构

loader 就是一个函数。接收一个参数 sourcesource 就是符合 test 中正则的模块内容,本例中就是 .js 后缀的文件内容。

  • 这个函数不可以是 箭头函数,因为 webpack 提供了 loader api 都是挂载到 this 上的。
  • 这个函数必须有 返回值,否则会报错。
// a-loader.js
module.exports = function(source) {
  console.log('🚀🚀~ source:', source);
  return source
}

src/index.js 文件中只输出一行日志。

// src/index.js
console.log('hello, world');

source 打印出来看一下,source 就是 src/index.js 这个文件还没有经过 webpack 编译的内容。

【初学者笔记】前端工程化必须要掌握的 webpack

loader的编写

a-loader.js 中,我们把 hello 替换成 你好,再返回。

// a-loader.js
module.exports = function(source) {
  const _source = source.replace('hello', '你好' )
  return _source
}

看一下打包后的 chunk , 变成了 console.log('你好, world'),说明我们自定义的 loader 已经生效了。

【初学者笔记】前端工程化必须要掌握的 webpack

在我们使用第三方 loader 的时候,通常会定义一个配置项 options ,那么我们的自定义 loader 怎么设置这个 options 呢?

首先还是像其它 loader 一样在 use 中设置一个 options 属性。

// webpack.config.js
module: {
    rules: [
      {
        test: /\.js$/,
        use: [
         {
            loader: path.resolve(__dirname, './myLoaders/a-loader.js'),
            options: {
              name: "一尾流莺"
            }
          },
        ]
      },
    ]
  }

然后在自定义 loader 中通过 this.query 获取到 options 的内容,这也就说明了为什么 loader 不能是个箭头函数的原因了。

world 替换成我们设置的 options 中的 name

// a-loader.js
module.exports = function(source) {
  const _source = source.replace('world', this.query.name )
  return _source
}

好,成功替换。

【初学者笔记】前端工程化必须要掌握的 webpack

loader 中进行异步操作是很正常的事,那么我们需要使用 this.callback ,一个可以同步或者异步调用的可以返回多个结果的函数。

this.callback(
  err: Error | null,
  content: string | Buffer,
  sourceMap?: SourceMap,
  meta?: any
);
  • 第一个参数必须是 Error 或者 null
  • 第二个参数是一个 string 或者 Buffer
  • 可选的:第三个参数必须是一个可以被这个模块解析的 source map
  • 可选的:第四个选项,会被 webpack 忽略,可以是任何东西(例如一些元数据)。

我们还需要通过 this.async 告诉 loader 的解析器这个 loader 将会异步地回调。

新建 myLoaders/b-loader.js 文件,使用 setTimeout 来模拟一段异步操作。

// b-loader.js
module.exports = function(source) {
  const _callback = this.async();
  const _source = source.replace('world', this.query.name)
  setTimeout(() => {
    _callback(null, _source)
  }, 3000)
}

然后引用 loader

// webpack.config.js
module: {
    rules: [
      {
        test: /\.js$/,
        use: [
         {
            loader: path.resolve(__dirname, './myLoaders/b-loader.js'),
            options: {
              name: "一尾流莺"
            }
          },
        ]
      },
    ]
  }

看一下构建时间,确实是三秒多。

【初学者笔记】前端工程化必须要掌握的 webpack

结果也如我们所愿,把 world 替换成了我的名字 一尾流莺

【初学者笔记】前端工程化必须要掌握的 webpack

异步 loader 不会影响其他模块的 loader,但是会影响多个 loader 作用于一个模块的 loader

一个 loader 执行完,才会交给下一个 loader 直到没有 loader ,最后交给 webpack

我们来测试一下两个 loader 作用于同一个模块的情况,由于 loader 的运行顺序是从右到左,所以我们先写 b-loader ,后写 a-loader

// webpack.config.js
module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: path.resolve(__dirname, './myLoaders/b-loader.js'),
            options: {
              name: "一尾流莺"
            }
          },
          {
            loader: path.resolve(__dirname, './myLoaders/a-loader.js'),
          },
        ]
      },
    ]
  }
// b-loader.js
module.exports = function(source) {
  const _callback = this.async();
  const _source = source.replace('world', this.query.name)
  setTimeout(() => {
    _callback(null, _source)
  }, 3000)
}
// a-loader.js
module.exports = function(source) {
  const _source = source.replace('hello', '你好')
  return _source
}

可以看到,两个 loader 的作用都生效了。

【初学者笔记】前端工程化必须要掌握的 webpack

但是有个问题,我们自定义的 loader 用绝对路径的引用方式,实在是太繁琐了,也不已读。

解决方案就是上面提到过的配置参数 resolveLoader ,这样就可以像使用第三方 loader 一样直接写 loader 的名字了。

// webpack.config.js
 resolveLoader: {
    // 默认是 node_modules
    modules: ["node_modules", "./myLoaders"]
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: "b-loader",
            options: {
              name: "一尾流莺"
            }
          },
          {
            loader: "a-loader",
          },
        ]
      },
    ]
  }

练习手写 loader

my-style-loader

module.exports = function(source) {
  return `
    const tag = document.createElement("style")
    tag.innerHTML = ${source}
    document.head.appendChild(tag)
  `
}

my-css-loader

module.exports = function(source) {
  return JSON.stringify(source)
}

my-less-loader

const less = require("less")
module.exports = function(source) {
  less.render(source, (error, output) => {
    const cssInfo = output.css
    this.callback(error, cssInfo)
  })
}

处理静态资源

前面提到过除了 .js.json 之外的文件类型,webpack 是不认识的,所以我们处理静态资源的时候,也是需要使用不同的 loader 的。

接下来我们将学习新的 loader 来处理图片资源。

file-loader

作用:导出一个资源,并返回路径。

配置项(options):

  • name :图片名称,可以使用占位符。[name] 名称,[ext] 后缀。
  • outputPath :输出资源的存储位置,默认相对于 dist 目录。
  • publicPath :资源的使用位置,publicPath + name = css中图片的完整使用路径
// webpack.config.js
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif|webp)$/,
        use: [
          {
            loader: "file-loader",
            options: {
              name: "[name].[ext]",
              outputPath: "images",
              publicPath: "../images",
            }
          },
        ]
      },
    ]
  }

url-loader

作用: 依赖 file-loader ,把图片转成 base64 编码,可以减少图片请求。

配置项(options):

  • name :图片名称,可以使用占位符。[name] 名称,[ext] 后缀。
  • outputPath :输出资源的存储位置,默认相对于 dist 目录。
  • publicPath :资源的使用位置,publicPath + name = css中图片的完整使用路径
  • limit : 超过指定大小的图片不会被转化成 base64 ,单位字节。
// webpack.config.js
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif|webp)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              name: "[name].[ext]",
              outputPath: "images",
              publicPath: "../images",
              limit: 4 * 102, 
            }
          },
        ]
      },
    ]
  }

image-webpack-loader

这个 loader 需要使用 cnpm 才能安装成功。

cnpm i image-webpack-loader -D

作用:图片压缩

// webpack.config.js
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif|webp)$/,
        use: [
          {
            loader: 'image-webpack-loader',
          }
        ]
      },
    ]
  }

看一下源文件和输出后的文件对比,效果还是很显著的。

【初学者笔记】前端工程化必须要掌握的 webpack

webpack5中的图片处理方式

// webpack.config.js
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif|webp)$/,
        // asset  通用配置 默认以 8kb 为界限 超过使用 asset/resource 否则使用 asset/inline
        // asset/resource 等同于 file-loader 的作用
        // asset/inline 等同于 url-loader 的作用
        type: "asset/resource",
        // 只有在 asset/resource 才可以设置 generator 属性
        generator: {
          filename: "images/[name][ext]",
        }
        parser: {
          dataUrlCondition: {
            // 只有在 asset 设置 maxSize 属性才会生效
            maxSize: 1 * 1024
          }
        }
      }
    ]
  }

字体文件的处理

字体文件是这样引用的。

// index.less
@font-face {
  font-family: 'webfont';
  font-display: swap;
  src: url('../font/webfont.eot'); /* IE9 */
  src: url('../font/webfont.eot?#iefix') format('embedded-opentype'),
    /* IE6-IE8 */ url('../font/webfont.woff2') format('woff2'),
    url('../font/webfont.woff') format('woff'),
    /* chrome、firefox */ url('../font/webfont.ttf') format('truetype'),
    /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
      url('../font/webfont.svg#webfont') format('svg'); /* iOS 4.1- */
}
body {
  div {
    height: 100px;
    background: blue;
    display: flex;
    background: url(../images/main.jpg) 0 0 no-repeat;
    font-family: 'webfont' !important;
  }
}

字体文件的处理和图片是一样的,用的 loader 也是一样的。

字体文件也可以转成 base64 格式的,所以也可以用 url-loader

// webpack.config.js
  module: {
    rules: [
      {
        test: /\.(eot|svg|ttf|woff|woff2)$/,
        use: [
          {
            loader: "file-loader",
            options: {
              name: "[name].[ext]",
              outputPath: "font",
              publicPath: "../font",
            }
          },
        ]
      },
    ]
  }

处理JavaScript模块

babel

首先要同时安装下面两个,babel-loader 依赖 @babel-core

npm i @babel-core babel-loader -D

babelpostcss 很像,是一个工具集,他自己不会做任何事情,需要依靠他自身的插件。

babel 也是一个可以用到浏览器集合的工具。

@babel/preset-env

用来处理 es6+ 语法的兼容问题。

我们先写点 es6 的新东西。

// index.js
const arr = [new Promise(() => { })]

然后配置插件。

// webpack.config.js
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ["@babel/preset-env"]
            }
          }
        ]
      },
    ]
  }

打包看看结果。

【初学者笔记】前端工程化必须要掌握的 webpack

可以看到,const 已经被转成了 var , 但是 promise 并没有被转化。因为上面已经说过了,这个插件只用于转化 es6+ 语法,而新特性是管不到的。

那怎么办呢?当然还要依赖其他的小伙伴了。

@babel/polyfill

这个包是在运行时起作用的,所以要安装到生产依赖里。

 npm i @babel/polyfill -S

这个包里包含了所有新特性,要在所有代码之前引用。

// index.js
import "@babel/polyfill"
const arr = [new Promise(() => { })]

babel7.4 以后的版本中,建议使用新的方式。

// index.js
import "core-js/stable"; 
import "regenerator-runtime/runtime";
const arr = [new Promise(() => { })]

但是由于这个包过大,所以我们还需要配置按需引入,这还是要通过 @babel/preset-envoptions 来配置。

// webpack.config.js
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                ["@babel/preset-env", {
                  // 单独指定浏览器集合
                  targets: {},
                  // 指定core-js的版本 需要安装 npm i core-js@3 -S
                  corejs: 3,
                  /**
                   * false 当我们引入polyfill,不会进行按需引入,bundle体积会很大
                   * entry 需要在入口文件里导入 import "@babel/polyfill" babel会帮我们按需引入
                   * usage 全自动检测 不需要导入polyfill 即可达到按需引入的目的
                   */
                  useBuiltIns: "usage"
                }]
              ]
            }
          }
        ]
      },
    ]
  }

babel 也可以有自己的配置文件 。 根目录下新建 babel.config.js 文件,把 options 中的内容挪过来即可,当然也要作为模块导出。

// babel.config.js
module.exports = {
  presets: [
    ["@babel/preset-env", {
      targets: {},
      corejs: 3,
      useBuiltIns: "usage"
    }]
  ]
}

集成react

集成 react ,安装依赖。

npm i react react-dom -S 
npm i @babel/preset-react -D

配置 babel.config.js

// babel.config.js
module.exports = {
  presets: [
    ["@babel/preset-react", {}]
  ]
}

集成vue

集成 vue ,安装依赖。这个其实跟 babel 没啥关系,顺便在这写了。

要注意 vuevue-template-compiler 的版本要保持一致。

npm i vue vue-loader vue-template-compiler -D

配置 webpack.config.js

// webpack.config.js
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  module: {
    rules: [
      // ... 其它规则
      {
        test: /.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  plugins: [
    // 请确保引入这个插件!
    new VueLoaderPlugin()
  ]
}

实现自定义plugin

plugin的结构

根目录下新建 myPlugins/my-plugin.js

plugin 的实质就是一个类。通过构造函数接收 options 配置。每个 plugin 必须内置一个 apply 方法,该方法接收一个参数 compiler ,就是实例化的 webpack ,其中包含配置等信息。

// my-plugin.js
class MyPlugin {
  constructor(options) {
    console.log('🚀🚀~ options:', options);
  }
  apply(compiler) {
    // console.log('🚀🚀~ : my-plugin');
  }
}
module.exports = MyPlugin

plugin的引用

新建实例的方式引用,可以在创建实例的时候传递 options

// webpack.config.js
const MyPlugin = require("./myPlugins/my-plugin")
module.exports = {
  plugins: [
    new MyPlugin({
      name: '一尾流莺'
    })
  ]
}

plugin的编写

怎么确定 plugin 的执行时机?

其实 webpack 也是有生命周期钩子的。我们可以来看一下都有哪些钩子。

// 跟目录下新建 webpack.js  然后通过 node webpack.js 启动
// 引入 webpack
const webpack = require("webpack")

// 引入配置文件
const config = require("./webpack.config.js")

// 根据配置文件 生产 webpack 实例
const compiler = webpack(config)

// 打印 webpack 的 生命周期钩子
Object.keys(compiler.hooks).forEach((hookName) => {
  compiler.hooks[hookName].tap("xxx", (compilation) => {
    console.log('🚀🚀~ hookName:', hookName);
  })
})

// 执行 编译
compiler.run()

所有的生命周期钩子都在 compiler.hooks 属性上,所以我们遍历打印出来的结果如下:

【初学者笔记】前端工程化必须要掌握的 webpack

可以看到 webpack 的生命周期钩子实在是太多了。

所以我们的 plugin 的事件只需要注册在对应的生命周期钩子上就可以了,然后 webpack 就会在恰当的时机通过我们自己的 plugin 了。

class MyPlugin {
  constructor(options) {
  }
  // 接收一个参数 compiler 就是实例化的webpack 包含配置等信息
  apply(compiler) {
    // 注册事件  同步钩子用tap  异步钩子用tapAsync注册
    // 事件名称可以为任意值,建议和插件名称保持语义一致
    compiler.hooks.emit.tapAsync('xxx', (compilation, cb) => {
      // compilation 半成品
      console.log('🚀🚀~ compilation:', compilation.assets);
      const content = 'hello,plugin  xxxxx'
      // 添加静态资源
      compilation.assets['warbler.txt'] = {
        source: function() {
          return content
        },
        // 只是用来查看的,并不会影响文件真实大小
        size: function() {
          return content.length
        }
      }
      cb()
    })
  }
}
module.exports = MyPlugin

通过 apply 方法的 compiler 参数进行事件的注册,可以注册在任何一个生命周期中,但是有的生命周期是同步的,有的是异步的,具体的还是要查看官网。

同步钩子用 tap 注册事件, 异步钩子用 tapAsync 注册事件。

如果是异步钩子的话, 需要在事件的回调中传递一个 cb 参数,并在最后调用一下。

事件名称可以为任意值,建议和插件名称保持语义一致,方便阅读源码,理解功能。

事件回调的 compilation 参数就是上一个钩子执行过后的半成品,属性有很多,可以阅读官网。