webpack基础

207 阅读7分钟

了解 webpack

  • 在以前,我们开始写一个 web 网页的时候,一般是直接一个 html 文件,引入一个 js 就可以开始写逻辑,但随着前端技术的发展,js 能实现的功能越来越多,工程的复杂性指数性增长,像原本那样的面向过程的编写方式最终会使得代码不可维护,这时我们迎来了面向对象开发
  • 从面向过程到面向对象,我们把每块逻辑分成不同的类,放在不同的 js 文件中,每个类的职责是固定的,他专注自己的工作,这样维护起来就方便很多,哪一块出了问题,我们只需要找到对应的类进行修改就可以了
  • 这样的方式虽然帮我们解决了一部分的问题,但同时也带来了新的问题:
    • 把代码拆分后,请求增加,使得页面加载变慢
    • 不能直接找到每个 块 跟 文件 的位置关系
    • 没有明确的依赖关系,这导致了我们在编写代码时还需要时刻注意是否加载了某个 js 文件,且不能一目了然的知道该从哪找到某个依赖
  • 有没有一种可能,我们只引入一个 js 入口文件,这个 js 文件里面自己需要什么自己再加载什么?我们很容易的想到了模块化开发,他可以解决以上的问题
  • 然而,在旧版本的浏览器,是不支持 ESM 的,这时,我们就需要一个 模块打包工具,把模块编译成它们能读出的东西,webpack 就是这么一个 模块打包工具
  • 在 webpack1.x 的版本,它只支持 js 模块的打包,现在,它已经可以支持大多数不同的模块了,例如:vuetsjsonjpg/png 等等
  • 除此之外,webpack 社区内还有很多插件,帮助我们维护工程
  • 有一点十分重要,我们在查阅 webpack 文档时,最好是中英文对照查阅,中文文档时常会有漏缺或者是落后版本的情况

webpack 安装

全局安装

npm install webpack webpack-cli -g
  • 不推荐全局安装,如果我们现在有两个项目需要同时开发,一个需要4.x版本,一个需要5.x版本,全局版本会固定,我们不可能时常修改全局的版本

局部安装

npm init -y
npm install webpack webpack-cli -D
  • 局部安装 webpack,可以在 package.jsonscript 配置启动
"scripts": {
  "build": "webpack"
},
  • 还可以使用 npx webpack 命令,npx 会在当前目录下运行存在于这个目录下的安装包

配置文件

  • webpack 为了吸引用户(我猜的),宣传的是 0 配置 打包,即你在工程目录下安装好 webpack 后,指定 js 文件打包,或有路径 /src/index.js 时,会自动生成 /dist/main.js
  • 但 webpack 实际上是需要配置文件去告诉它,该如何打包,0配置打包仅仅是它内置了配置的默认项,而配置文件的本质实际上就是 导出的对象
const path = require('path');

module.exports = {
  entry: './src/index.js', // 打包入口文件 单入口:string|Array<string> 多入口:Object
  // output:产品输出,存放位置、名称
  output: {
    // publicPath: 'http://cdn.com.cn', // 如果要加入绝对路径域名一般都是 这个字段
    path: path.resolve(__dirname, './dist'), // 存放位置,绝对路径
    filename: '[name].js', // 名称 [name] 为占位符,对应 entry 为对象时的 key
    chunkFilename: '[name].js', // 非入口 chunks 名称,一般为异步请求
  },
  // mode:打包配置
  mode: 'production', // production development none
}
  • webpack 的配置项默认为工程目录下 webpack.config.js
  • 如果我们需要其他路径名称作为它的配置项,则需要通过 --config 进行指定
npx webpack --config ./myConfig.js

webpack 打包输出内容及基本概念

  • 学习 webpack 有一些基本概念需要了解,这些东西能让我们在打包时更快发现问题,一些文档也会牵涉这些概念
  • 在我们打包之后,我们会看到控制台有一堆东西出来
    1. Hash:此次打包唯一哈希值
    2. Version:此次打包版本
    3. Time:此次打包耗时
    4. Asset:打包出来的资源文件
    5. Size:打包出来的文件大小
  • 上面这些都比较好理解,但是有个陌生的名字 chunk,这是个什么东西,什么又是 bundle 文件?
  • webpack 是个 模块打包工具,那么这些东西就肯定或多或少的跟 模块有关系
    1. Module:模块
    2. chunk:代码块 - 经过 webpack 处理过的小块
    3. chunks:chunk 组
    4. Chunk Names:chunk名
    5. bundle:经过 webpack 流程 解析编译 处理后的最终输出成果文件
      • 包含:自执行函数(函数 和 chunks)
      • 一个 bundle 就有一个 chunks
      • 一个 chunks 至少有一个 chunk == module

loader

  • 在默认情况下 webpack 默认只支持 js、json 语法(注意不是文件后缀)
  • 如果想支持其他模块打包就需要使用 loader,它的作用就是告诉 webpack 该模块是如何编译的
  • loader 在下载之后在 webpack.config.js 内配置使用
module: {
  rules: [{ // 规则
    test: /\.xxx$/, // 正则匹配文件
    use: { // 使用哪个 loader
      loader: 'xxx-loader'
    }
  }]
}
  • loader 有非常的多,当我们使用到一个非 js 或 json 模块时,我们就可以去官网查找相关 loader 对模块进行解析 loaders
  • webpack 官方建议,一个 loader 只做一件事情,小而美,所以我们在编译一个模块时可能经历多个 loader,use 就是数组,顺序为 自下而上,或是 从右往左
module: {
  rules: [{ // 规则
    test: /\.xxx/, // 正则匹配文件
    use: ['xxx-loader', 'xxx-loader'],
    use: [{
      loader: 'xxx-loader',
      options: {}
    }, {
      loader: 'xxx-loader',
      options: {}
    }]
  }]
}

使用 loader 打包静态资源

图片、字体等文件

module: {
  rules: [{ // 规则
    test: /\.(jpe?g|png|webp|gif|woff2?|...)$/, // 正则匹配文件
    use: { // 使用哪个 loader
      loader: 'url-loader',
      options: { // 配置参数 - 详情需要看文档
        name: '[name].[ext]',
        outputPath: 'images',
        limit: 3 * 1024, // 3kb 没有超过阈值会转换成 base64
      }
    }
  }]
}
  • url-loader 的使用与 file-loader 类似,不过多了一个把过小文件(limit)编译成 base64 的功能,url-loader 依赖 file-loader

样式

  • style-loader - 把编译后的 css 挂在 style 标签内 输出到 <head>(可指定输出位置)
  • css-loader - 解析 css 依赖关系(@import),压缩,序列化 css 代码
  • postcss-loader - 将 css 使用 postcss 进行样式转换
  • sass-loader - 把 scss 编译成 css
module: {
  rules: [{
    test: /\.scss$/,
    // 这里能看的出来,loader的执行是按顺序的,我们需要先编译 sass,再使用 postcss,再序列化,再输出
    use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
  }]
}
  • postCss 可以编译处理 css 模块,可以说的上是 css 版的 babel,功能也是十分的强大,它是一个工具,有很多个插件,具体干活的就是插件
  • 既然它有这么多插件干活,全堆在 webpack.config.js 里也是太臃肿了,所以它也有自己的一个 配置文件 - postcss.config.js
module.exports = {
  // 根据 目标浏览器集合,自动添加补齐css3特性
  plugins: [
    require('autoprefixer')({ // 浏览器前缀
      overrideBrowserslist: ["last 1 version"] // 目标浏览器集合
    }),
    require('cssnano'), // css 压缩
  ]
}
  • overrideBrowserslist 可以单独配置目标浏览器集合,不过推荐还是使用 Browerslist

样式配置补充 - 防止漏编译 和 css 模块化

  • css-loader 在解析到 @import 语句时虽然引入了,但是有可能引入的 css 没有经过前面的编译,在配置时加入 importLoaders 标明在 css-loader 前应用的 loader 的数量可以防止漏编译
  • js 中 import css 会全局使用 css,还有一种方法可以让 css 只在该模块下产生作用,称为 css 模块化,在 css-loader 加上 modules 为 true 就可以开启
import css from './css.scss';

const div = document.createElement('div');
div.classList.add(css.class); // 该类名 class 样式仅在此模块有效

document.documentElement.appendChild(div);
module: {
  rules: [{
    test: /\.scss$/,
    use: ['style-loader', {
      loader: 'css-loader',
      options: {
        importLoaders: 2, // 在 css-loader 前应用的 loader 的数量
        modules: true // 启用 CSS 模块
      }
    }, 'postcss-loader', 'sass-loader']
  }]
}

plugins

  • loader 负责把浏览器不认识的模块进行编译,那么如果我除了编译职位还想在打包时做一些别的事情,例如自动生成 html 文件,把 css 文件单独抽离,拆分 chunk 等等操作该怎么办
  • 插件(plugins) 这个时候就可以帮助我们在 webpack 的打包生命周期中,帮我们做一些事情,下面举一些常用的插件,试一下使用它们
  • 一般的使用方法都是在 webpack.config.js 引入使用
const xxxPlugin = require('xxx-plugin');

module.exports = {
  plugins: [
    new xxxPlugin()
  ]
}

clean-webpack-plugin

  • 在生成 Asset 前清空 文件夹
  • 假如我们对一些资源进行删除,在每次打包前都需要手动删除一下,这个插件就可以帮助我们自动清理输出目录中的所有文件
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
  plugins: [
    new CleanWebpackPlugin()
  ]
}

html-webpack-plugin

  • 简化 html 文件的创建,因为 webpack 打包的是模块,而我们打包后需要将 js 等资源文件引入,使用这个插件就不需要我们每次都手动重新写 html 文件,它也会自动帮助我们引入资源
  • 同时我们可以借用模板,使用类似于 jsp 的方式引入一些变量
<%= htmlWebpackPlugin.options.div %>
new HtmlWebpackPlugin({
  div: '<div>哈哈哈哈哈哈</div>',
  template: './src/index.html'
})

mini-css-extract-plugin

  • 将 CSS 提取到单独的文件中
  • 注意:这个插件自带一个 loader,在 css 的编译中要使用它替换 style-loader
const miniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  module: {
    rules: [{
      test: /\.css$/,
      use: [{
        loader: miniCssExtractPlugin.loader,
        options: {
          publicPath: '../'
        }
      }, 'css-loader', 'postcss-loader'],
    }]
  },
  plugins: [
    new miniCssExtractPlugin({
      filename: 'css/[name].css'
    })
  ]
}

Browerslist

  • 不管是 postcss 还是 babel,都是需要知道目标浏览器集合,才可以按照我们的需求进行兼容处理
  • 一般我们配置时很少需要某块单独兼容,所以推荐集合统一配置在 Browerslist
  • 作用:声明一段浏览器集合,工具可以根据这段集合描述,输出兼容性的代码
  • 来源:Can I use
  • 工具:browserslist
  • 集合语句:Full List

package.json

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

.browserslistrc

  • 根目录下创建 .browserslistrc 文件,直接写出集合,以换行分隔
last 2 version
> 1%

检测、查询浏览器集合

  • 使用 npx browserslist 命令
    • 不写参数:工具会主动查找 Browerslist 配置输出集合,如无配置输出 defaults 内容
    • 写参数:工具根据参数生成一个文件,里面有该参数的浏览器集合
npx browserslist
npx browserslist ">1%"

devtool - sourceMap

  • 不管是 js 代码还是 css 代码,经过 webpack 打包过后都会变得面目全非,压缩、合并,使得我们没有办法去很好的 debug 到原本问题出现的地方,这个时候我们需要 source map
  • source map 本质上就是一个信息文件,里面储存着代码转换前后的对应位置信息,记录了转换压缩后的代码所对应的转换前的源代码位置,是源代码和生产代码的映射,让浏览器的调试面版将生成后的代码映射到源码文件当中
  • 在 webpack 中,配置 devtool 就可以配置 source map
  • 因为生成 source map 文件需要时间,它会影响 构建/重新构建 的速度,我们可以改变 devtool 的参数对照着官方给出的快慢速度找到我们需要的配置
module.exports = {
  devtool: 'source-map',
}
  • 配置选项也是挺多的,我们找关键字,看看它们有什么不同
    • source-map:单独打包 map 文件,以 sourceMappingURL=main_xxxxx.js.map 引入
    • inline:不单独打包出 map 文件,直接以 base64 的字符串被写在输出代码中
    • cheap:
      • 一般报错会告诉我们哪一行哪一个字符出错了,cheap 只定位到行,从而提升打包性能
      • 只针对业务代码,不会管引入的第三方模块或者库错误
      • module:还管引入的第三方模块或者库错误
    • eval:
      • 打包速度最快,性能最好,但是代码复杂不适合用,打包出来既然没有 map 文件,也没有 base64 的字符串
      • 每一个模块文件都转换为字符串使用 eval 执行,并且在每一个模块代码的尾部添加 //# sourceURL=webpack:///xxx.js 形成映射关系
  • 总结:
    • modedevelopment 时推荐使用 cheap-module-eval-source-map,报错比较全面,打包速度可观
    • modeproduction 时可以看下 对于生产环境 按照想要的需求进行配置
    • 注意webpack 5.x 的值略有不同(大概是改了下顺序),使用时可去官网查一下

webpackDevServer

  • 我们使用 webpack 开发时,每次都手动打包之后才能看到效果,这样的开发效率是很低的,我们需要在改变代码的同时,webpack 观测到后自动打包,提升开发效率
  • 有三种方法可以实现我们的需求,因为 devServer 已经十分的成熟,建议还是使用 devServer

watch

npx webpack --watch
"scripts": {
  "watch": "webpack --watch"
}

devServer

  • devServer 并不会生成 Asset,而是会将它保存在内存里面,从而提升打包速度
npm install webpack-dev-server -D
  • 默认配置,最简单的使用方法
npx webpack serve
  • webpack.config.js 配置
"scripts": {
  "serve": "webpack-dev-server"
}
module.exports = {
  devServer: {
    static: {
      directory: path.join(__dirname, 'dist'), // 服务器根路径,告诉服务器从哪里提供内容
    },
    // contentBase: path.join(__dirname, 'dist'), // webpack 4.x 字段,与 static 相同
    compress: true, // 启用压缩
    open: true, // 为 true 时,启用 devServer 会自动打开浏览器
    proxy: {
      '/api': 'http://localhost:3000', // 请求代理 - /api/users -> http://localhost:3000/api/users
      pathRewrite: { '^/api': '' }, // 重写,去除/api - /api/users -> http://localhost:3000/users
    },
    port: 8080, // 端口号
  },
}

使用 koa|express + webpack-dev-middleware 自己写一个服务器

  • 下面的例子只是一个简化版的,如果要实现 devServe 还需要大量的工作,这里只做一个了解
  • Node接口
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config');
// 在 node 中直接使用 webpack
const complier = webpack(config); // 编译器,运行一次 webpack 就会帮我们打包一次代码

const app = express();
app.use(webpackDevMiddleware(complier, {
  publicPath: config.output.publicPath
}));

app.listen(8080);

HMR - 模块热替换

  • Hot Module Replacement

  • 我们在使用 devServe 时,每次保存内容后webpack 会帮我们自动打包且刷新浏览器,如果当我们页面已经操作了很多步之后需要修改一个与之不相关的模块或仅仅是改一下样式时,就需要重新操作一遍,那肯定会非常的麻烦

  • 使用 HMR 就可以大大简化这个流程,它允许在运行时更新各种模块,而无需完全刷新

  • 它的原理也并不复杂,就是在浏览器加载页面之后建立 WebSocket 连接,当文件变化后,发送 hash 事件到浏览器,浏览器加载变更模块,再运行回调

const webpack = require('webpack');

module.exports = {
  devServer: {
    hot: 'only', // 为 true 时:开启 HMR,为 'only' 时: 即使打包报错,浏览器也不会自动刷新
    // hotOnly: true, // webpack 4.x 的 hot: 'only'
  },
  optimization: {
    moduleIds: 'named', // webpack 5.x 时的 webpack.NamedModulesPlugin
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    // new webpack.NamedModulesPlugin(), // 查看 HMR 修改的依赖
  ]
}
  • 除此之外,我们还需要 HMR Api 提供的钩子去对修改的模块进行下一步的操作
  • 一般使用的比较多的是 accept,在模块更改之后的接受钩子
import xxx from './xxx'

if (module.hot) {
  module.hot.accept('./xxx.js', () => {
    xxx();
    ...something
  });
}
  • 当然这很麻烦,幸运的是我们常用的 style-loader 或框架的 vue-loader react-hot-reload 都内置了模块热更接口,但是如果我们引入一些比较偏的文件时,还是需要自己写一下

Babel

  • 在我们使用 es6+ 又要兼容低版本浏览器,又或者使用 JSX TS 时,我们必须对 js 进行编译处理,而使用到的工具就是 Babel
  • Babel 就是一个 JavaScript 编译器,可以对我们的代码做兼容输出
  • 我们可以在很多平台上使用 Babel,比如 浏览器、CLI工具,我们可以看下 协同工作 不同平台下如何使用它

基本使用

npm install babel-loader @babel/core @babel/preset-env -D
module: {
  rules: [
    {
      test: /\.m?js$/,
      exclude: /node_modules/, // 排除 node_modules,第三方工具一般已经帮我们做了兼容性处理
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}
  • 注意@babel/preset-env 只能帮我们处理语法问题,不能处理缺失的特性,这二者的不同,下面举个栗子
    • 箭头函数 - 语法
    • Promise - 特性

特性补齐

  • 使用 polyfill 补齐缺失的特性,它是一个 js 库,提供了由 es5 方式实现的新特性
  • 直接使用(core-js 2.x)
npm install @babel/polyfill
// 入口文件开头直接引入
import '@babel/polyfill'
  • Babel 7.4.0 版本后,@babel/polyfill 被弃用(core-js 3.x)
npm install core-js regenerator-runtime
// 入口文件开头直接引入
import 'core-js/stable';
import 'regenerator-runtime/runtime';
  • 按需加载

  • 配置 @babel/preset-env 时使用参数 useBuiltIns

    • usage:全自动,不需要引入,打包时会根据使用到的特性进行补全
    • entry:需要手动引入 polyfill,会根据 browserslist 补全目标浏览器不兼容的所有特性
npm i core-js
module: {
  rules: [{
    test: /\.m?js$/,
    exclude: /node_modules/,
    use: {
      loader: "babel-loader",
      options: {
        presets: [['@babel/preset-env', {
          useBuiltIns: 'usage', // 全自动模式
          corejs: 3, // core-js 版本
          targets: 'defaults' // 目标浏览器集合,建议写在 .browserslistrc
        }]]
      }
    }
  }]
}

.babelrc & babel.config.js

  • 在使用 Babel 时,因为工具过多,如果全放在 options 内,webpack.config.js 就会变得越来越长,不好管理,Babel 也有自己的配置文件
  • .babelrcbabel.config.js 都是 Babel 的配置文件,babel.config.jsBabel 7.x 的新特性,它的由来是因为项目范围问题,具体不同可以看下 config-files
  • 可以粗理解为 全局配置(babel.config.js) 和 局部配置(.babelrc)
module: {
  rules: [
    {
      test: /\.m?js$/,
      exclude: /node_modules/,
      use: 'babel-loader'
    }
  ]
}
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

第三方模块开发时特性补全

  • 由于 @babel/polyfill 的特性补全是通过全局注入的方式实现的,所以当我们开发第三方库时,使用它就会产生全局污染问题,我们需要换一种方式实现 transform-runtime
npm install @babel/plugin-transform-runtime -D
:: @babel/runtime-corejs 可以选择版本 2 || 3
npm install @babel/runtime @babel/runtime-corejs3
{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "absoluteRuntime": false,
        "corejs": 3,
        "helpers": true,
        "regenerator": true,
        "version": "7.0.0-beta.0"
      }
    ]
  ]
}