先简略回顾下 webpack 原理

Webpack 能够看做是模块打包机,把解析的所有模块变成一个对象,而后通过入口模块去加载咱们的货色,而后顺次实现递归的依赖关系,通过入口来运行所有的文件。因为 webpack 只意识js,所以须要通过一系列的 loaderplugin 转换成适合的格局供浏览器运行。

  • loader 次要是对资源进行加载/转译的预处理工作,其本质是一个函数,在该函数中对接管到的内容进行转换,返回转换后的后果。某种类型的资源能够应用多个 loader,执行程序是从右到左,从下到上。
  • plugin(插件)次要是扩大 webpack 的性能,其本质是监听整个打包的生命周期。webpack 基于事件流框架 Tapable, 运行的生命周期中会播送出很多事件,plugin 能够监听这些事件,在适合的机会通过 webpack 提供的 API 扭转输入后果。

webpack 装置

新建一个目录,进入目录初始化 package.json,并装置 webpack 依赖

// 初始化包npm init -y// 装置依赖npm i webpack webpack-cli -D

根底配置

webpack 默认配置文件名字为 webpack.config.js,于是在我的项目根目录下新建一个名为 webpack.config.js 的文件,在配置文件里写最简略的单页面配置:

let path = require("path");module.exports = {  mode: "development",  entry: "./src/js/index.js",   output: {    filename: "js/bundle.js",     path: path.resolve("dist"),    publicPath: "http://cdn.xxxxx"  }}

配置详解

  • mode - 打包模式

    • development 为开发模式,打包后代码不会被压缩
    • production 为生产模式,打包后代码为压缩代码
  • entry - 入口文件
  • output - 打包文件配置

    • filename:打包后文件,filename 的值可设置成带 hash 戳的文件:js/bundle.[hash].js / js/bundle.[hash:8].js(只显示 8 位 hash 戳)
    • path:打包文件门路,需为绝对路径
    • publicPath:上线的cdn地址
TIP: 上述代码中 path 为内置模块,无需装置,间接引入即可。

新建后还需在我的项目根目录下的 src/js 目录下新建 index.js 文件,而后轻易输出一句 js 代码。

配置后可应用 webpack 命令尝试打包,若报错找不到命令可 npm i webpack -g 全局装置后再打包,打包胜利后会输入到我的项目根目录下的 dist 目录。

我的项目目录构造大抵如下

├─package.json├─webpack.config.js├─src|  ├─js|  | └index.js├─dist

html 文件打包

因为 webpack 只意识 js,因而需通过 html-webpack-plugin 插件打包 html 文件

npm i html-webpack-plugin -D

装置后在 webpack.config.js 配置文件中

let HtmlWebpackPlugin = require("html-webpack-plugin");module.exports = {    plugins: [        new HtmlWebpackPlugin({          template: "./src/index.html"        })    ]}

production 模式下能够开启 html 文件的压缩配置:

plugins: [    new HtmlWebpackPlugin({      template: "./src/index.html",      minify: { removeAttributeQuotes: true, collapseWhitespace: true },       hash: true    })]

配置详解

  • plugins - webpack 插件配置

    • html-wepack-plugin配置

      • template - html 模板文件的绝对/绝对路径
      • minify - 压缩配置

        • removeAttributeQuotes:删除属性双引号
        • collapseWhitespace:代码压缩成一行
      • hash - 引入文件带上hash戳
TIP: 如果不指定模板 template 配置,将是插件默认的 html文件,而不是我的项目中的 html 文件

开启服务

webpack 通过装置 webpack-dev-server 开启服务

npm i webpack-dev-server -D

配置 webpack.config.js

devServer: {    port: 5000,    compress: true,    open: true,    client: { progress: true }}

配置详解

  • devServer - webpack-dev-server 配置

    • port - 端口号
    • compress - 开启 gzip 压缩
    • open - 启动后主动把页面关上
    • client

      • progress:在浏览器中以百分比显示编译进度

配置好可运行 webpack-dev-server 命令查看成果,若找不到命令可 npm i webpack-dev-server -g 全局装置下

跨域

开发过程中容易遇到接口跨域问题,可通过 devServer.proxy 配置解决

假如接口地址为 http://localhost:3000/api/users,对 /api/users 的申请可如下配置

devServer: {    proxy: {      '/api': 'http://localhost:3000',    },},

但理论我的项目中接口的地址有很多种可能,个别不会有 /api 目录,即个别接口地址为http://localhost:3000/users,因而枚举配置会很麻烦,可通过代理申请解决

即先申请 http://localhost:3000/api/users 接口地址,而后通过 devServer 代理到 http://localhost:3000/users

本文通过 express 开启接口服务,接口地址为 http://localhost:3000/user,接口代码不再赘述,前期上传残缺的源码,可通过 node "我的项目门路\webpack5\src\js\server.js" 启动接口服务,而后配置 webpack.config.js

devServer: {    proxy: {      "/api": {        target: "http://localhost:3000/",        pathRewrite: {          "/api": ""        },      },    }}

devServer 配置详解

  • proxy - 代理配置

    • target - 接口域名
    • pathRewrite - 接口门路重写,把申请代理到接口服务器上

mock 接口数据

当后端接口没有写好,又不心愿被阻塞进度,能够通过 mock 后期跟后端约定好的接口数据格式来模仿调试页面。可应用有自定义函数和利用自定义中间件的能力的配置 devServer.setupMiddlewares,在 middlewares.unshift 中的回调函数应用 res.send 把须要 mock 的数据传递进去:参考webpack视频解说:进入学习

devServer: {    setupMiddlewares: (middlewares, devServer) => {        if (!devServer) {        throw new Error("webpack-dev-server is not defined");        }        middlewares.unshift({            name: "user-info",            // `path` 是可选的,接口门路            path: "/user",            middleware: (req, res) => {              // mock 数据模仿接口数据              res.send({ name: "moon mock" });            },        });        return middlewares;    },}

款式解决

款式解决须要用到的 loader 及其作用:

  • less-loader:加载和转译 LESS 文件
  • postcss-loader:应用 PostCSS 加载和转译 CSS/SSS 文件,如能够解决 autoprefixer css 包,为css增加浏览器前缀
  • css-loader:解析 @import and url() 语法,应用 import 加载解析后的css文件,并且返回 CSS 代码
  • mini-css-extract-pluginloader:抽取出 css 文件,通过 link 标签引入 html 文件

装置依赖,若应用的是 sass,则把 less less-loader 换成 node-sass sass-loader 即可

npm i mini-css-extract-plugin css-loader postcss-loader autoprefixer less-loader less -D

配置 webpack.config.js

const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = {    plugins: [        new MiniCssExtractPlugin({          filename: "css/main.css", // 抽离的css文件名        })    ],    module: {        rules: [            {                test: /\.css$/,                use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"],            },            {                test: /\.less$/,                use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader", "less-loader"],            },        ]    }}

还需新建并配置 postcss.config.js

module.exports = {  plugins: [require("autoprefixer")]};

上述文件配置好后,打包后会发现 css3 款式还是没有增加前缀,还需配置 package.jsonbrowserlist 能力失效

"browserslist": [    "last 1 version",    "> 1%",    "IE 10"],

js 解决及语法校验

es6 或更高级的语法需转化成 es5,并应用 eslint 标准代码:

  • babel-loader:加载 ES2015+ 代码,而后应用 Babel 转译为 ES5
  • @babel/preset-env:根底的ES语法分析包,各种转译规定的对立设定,目标是通知loader要以什么规定来转化成对应的js版本
  • @babel/plugin-transform-runtime:解析 generator 等高级语法,但不蕴含 include 语法,include 语法需装置 @babel/polyfill。官网文档说上线需带上 @babel/runtime 这个补丁,该包还做了一些办法抽离的优化,如 class 语法的抽离(抽离出 classCallCheck 办法)
  • @babel/polyfill:解析更高级的语法,如 promiseinclude 等,在js文件中 require 引入即可
  • eslint-loader:校验 js 是否符合规范,可自行在 eslint 网站上配置下载

装置依赖

npm i @babel/core babel-loader @babel/preset-env @babel/plugin-transform-runtime [email protected]/polyfill -Dnpm i @babel/runtime eslint-loader eslint -S

webpack.config.js

{    test: /\.js$/,    use: {      loader: "eslint-loader",      options: {        enforce: "pre", // 定义为前置loader,在normal的loader前执行      },    },},{    test: /\.js$/, // enforce 默认为 normal 一般loader    use: {      loader: "babel-loader",       options: {        presets: ["@babel/preset-env"], // 把es6转成es5        plugins: ["@babel/plugin-transform-runtime"], //作用?      },    },    include: path.resolve(__dirname, "src"),    exclude: /node_modules/,},

配置 source-map

源码映射配置 source-map 的值:

  • source-map 映射源码 会独自生成source-map文件 出错了会标识以后报错的行和列 大而全
  • eval-source-map 不会产生独自的文件,可显示行和列
  • cheap-module-source-map 不会标识列,会生成独自的映射文件
  • cheap-module-eval-source-map 不会产生文件 集成在打包后的文件中 不会产生列

webpack.config.js

  devtool: "eval-source-map",

引入js全局变量

有三种形式能够引入全局变量

expose-loader

可把变量裸露到 window 全局对象上,以 jquery 为例,先装置依赖

npm i jquery expose-loader -D

而后在 webpack.config.js 中配置 loader,把 $ 裸露到 window 全局对象上

module: {  rules: [{    test: require.resolve('jquery'),    use: [{      loader: 'expose-loader',      options: '$'    }]  }]}

除了上述办法外还能够在入口 js 文件中裸露

require("expose-loader?$!jquery");

providePlugin

可应用 webapck 内置插件 providePlugin 给每个模块中注入变量,还是以 jquery 为例

webapck.config.js 中配置

const webpack = require("webpack");module.exports = {    plugins: [        new webpack.ProvidePlugin({          $: 'jquery'        });    ]}

而后在任意js模块中能够间接应用$调用,无需引入jquery包

// in a module$('#item'); // <= works// $ is automatically set to the exports of module "jquery"

通过 cdn 引入

还能够通过 cdn 链接的形式引入全局变量,但如果此时js文件中多写了 import $ from 'jquery',就会把 jquery 也打包进去,可应用 external 避免将某些 import 的包(package)打包到 bundle 中

index.html

<script  src="https://code.jquery.com/jquery-3.1.0.js"  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="  crossorigin="anonymous"></script>

webpack.config.js

module.exports = {  //...  externals: {    jquery: 'jQuery',  },};

这样就剥离了那些不须要改变的依赖模块,换句话,上面展现的代码还能够失常运行:

import $ from 'jquery';$('.my-element').animate(/* ... */);

下面的例子。属性名称是 jquery,示意应该排除 import $ from 'jquery' 中的 jquery 模块。为了替换这个模块,jQuery 的值将被用来检索一个全局的 jQuery 变量。换句话说,当设置为一个字符串时,它将被视为全局的(定义在下面和上面)。

款式压缩和 js 压缩

production 模式下需压缩 css 可应用插件 css-minimizer-webpack-plugin,但应用了此插件压缩 css, 会导致 js 不压缩,所以须要装置 js 压缩插件 terser-webpack-plugin

const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");const TerserPlugin = require("terser-webpack-plugin");module.exports = {    optimization: {        minimize: true,        minimizer: [          new CssMinimizerPlugin(),          // 压缩js          new TerserPlugin({ test: /\.js(\?.*)?$/i }),        ],    },}

图片解决

须要 loader 解析图片资源:

  • file-loader:将文件的import/require()解析为url,并将文件发送到输入文件夹(dist文件夹),并返回(绝对)URL
  • url-loader:像 file-loader 一样工作,但如果文件小于限度,能够返回 data URL,即把图片变成base64
  • html-loader:能够解析html标签引入的图片,能够通过查问参数 attrs,指定哪个标签属性组合(tag-attribute combination)应该被解决,默认值:attrs=img:src

装置依赖

npm i file-loader url-loader html-loader -D

配置 webpack.config.js

module: {    rules: [      {        test: /\.jpg|png|jpeg$/,        use: {          loader: "file-loader",           options: {            outputPath: "images/",            name: "[name].[ext]", // 如果不写文件名,则会生成随机名字            // publicPath: "http://cdn.xxx.com/images", // 可配置生产环境的cdn地址前缀          },        },      },      {        test: /\.(html)$/,        use: {          loader: "html-loader",          options: {            esModule: false,           },        },      },    ]}
TIP: url-loader能够应用 options.limit 限度,小于多少k时应用base64转换,大于这个体积应用file-loader打包

html-loader 配置报错问题

html-loader 需敞开 es6 模块化,应用commonjs解析,否则会报错。起因次要是两个 loader 解析图片的形式不一样。

我的项目目录构造大抵如下

├─.eslintrc.json├─package-lock.json├─package.json├─postcss.config.js├─webpack.config.js├─src|  ├─index.html|  ├─js|  | ├─index.js|  | ├─server.js|  | └test.js|  ├─image|  |   └logo.png|  ├─css|  |  ├─a.css|  |  └index.css├─dist

resolve 配置

resolve 罕用的属性配置:

  • modules:通知 webpack 解析模块时应该搜寻的目录。绝对路径和相对路径都能应用,然而要晓得它们之间有一点差别。

    • 应用绝对路径,将只在给定目录中搜寻。应用相对路径,通过查看当前目录以及先人门路。
    • 如果想要优先于某个目标目录搜寻,则需把该目录放到目标目录后面,可详看官网例子
  • alias:设置别名,方便使用,上面的例子利用于 src 目录下的门路应用
  • mainFields:当从 npm 包中导入模块时(例如,import * as D3 from 'd3'),此选项将决定在 package.json 中应用哪个字段导入模块。依据 webpack 配置中指定的 target 不同,默认值也会有所不同。这里 browser 属性是最优先选择的,因为它是 mainFields 的第一项
  • extensions:尝试按程序解析这些后缀名。当引入的文件不带后缀名,且有多个文件有雷同的名字,但后缀名不同,webpack 会解析列在数组首位的后缀的文件 并跳过其余的后缀。
let path = require("path");module.exports = {    resolve: {        modules: [path.resolve("node_modules")],         alias: {          "@": path.resolve(__dirname, "src"),        },        mainFields: ["browser", "module", "main"],         extensions: [".js", ".json", ".vue"],     },}

多页面配置

多页面顾名思义就是多个 html 页面,因而个别也会有多个 js 入口文件。

上面的配置中 entry 的 key 值对应的是 output 属性的 [name] 值,HtmlWebpackPlugin 中的属性 chunks 示意引入 [name] 对应的 js 代码文件,不指定 chunks 值将引入所有打包进去的 js 文件。

即本例的 [name] 别离为 homeother,即打包进去是 home.js 和 other.js,最终打包的成果是 home.html 引入的是 home.jsother.html 引入的是 other.js 文件

配置 webpack.config.js

let path = require("path");let HtmlWebpackPlugin = require("html-webpack-plugin");module.exports = {  mode: "development",  entry: {      home: "./src/js/index.js",      other: "./src/js/other.js",  },   output: {    filename: "js/[name].js",     path: path.resolve("dist")  },  plugins: [    new HtmlWebpackPlugin({      template: "./src/index.html",      filename: "home.html",      chunks: ['home']    }),    new HtmlWebpackPlugin({      template: "./src/other.html",      filename: "other.html",      chunks: ['other']    }),  ],}

webpack 小插件利用

clean-webpack-plugin

革除插件,可用于革除上一次的打包文件,革除目录为 output.path 的值

装置依赖

npm i clean-webpack-plugin -D

配置 webpack.config.js

const { CleanWebpackPlugin } = require("clean-webpack-plugin"); module.exports = {    plugins: [        new CleanWebpackPlugin(),    ]}

copy-webpack-plugin

拷贝插件,把某个文件夹导出到打包文件夹中,如文档文件夹(如 doc 文件夹)

装置依赖

npm i copy-webpack-plugin -D

配置 webpack.config.js

const CopyWebpackPlugin = require("copy-webpack-plugin"); // 拷贝文件module.exports = {    plugins: [        new CopyWebpackPlugin({          patterns: [            {              from: "./doc",              to: "./doc",             },          ],        }),    ]}

插件配置属性

  • patterns

    • from: 源文件,绝对于当前目录门路
    • to:指标文件,绝对于output.path文件门路,会生成到 dist/doc 目录下

webpack.BannerPlugin

版权申明插件,webpack 内置插件,无需装置

配置 webpack.config.js

const webpack = require("webpack");module.exports = {    plugins: [        new webpack.BannerPlugin("copyright by Moon in 2022"),    ]}

watch

能够监听文件变动,当它们批改后会从新编译,能够用在实时打包的场景下

配置 webpack.config.js

watch: true,watchOptions: {  poll: 1000, //每秒查看一次变动  aggregateTimeout: 600, // 防抖  ignored: /node_modules/,},

配置属性

  • watchOptions 监听参数

    • poll: 每n毫秒查看一次变动
    • aggregateTimeout:防抖,当第一个文件更改,会在从新构建前减少提早。这个选项容许 webpack 将这段时间内进行的任何其余更改都聚合到一次从新构建里。以毫秒为单位
    • ignored:对于某些零碎,监听大量文件会导致大量的 CPU 或内存占用。能够应用正则排除像 node_modules 如此宏大的文件夹

配置后在命令窗口输出 npm run build 就能够进行监控并实时打包了,如图实时打包了一次

环境变量

通过 webpack 内置插件 DefinePlugin 定义 DEV 环境变量。

还能够把开发和生产模式不同的 webpack 配置抽离进去,即把 webpack.config.js 文件一分为三

  • 公共配置放在 webpack.config.base.js 文件
  • 开发模式配置放在 webpack.config.dev.js 文件,通过 webpack-merge 合并webpack.config.base.js 文件和 webpack.config.dev.js 文件的配置
  • 生产模式配置放在webpack.config.prod.js 文件 (和开发模式配置文件逻辑统一)

webpack.config.dev.js 文件残缺代码如下:

let { merge } = require("webpack-merge");let base = require("./webpack.config.base.js");let HtmlWebpackPlugin = require("html-webpack-plugin");const webpack = require("webpack");module.exports = merge(base, {  mode: "development",  devtool: "eval-source-map",  plugins: [    new HtmlWebpackPlugin({      template: "./src/index.html",    }),    new webpack.DefinePlugin({      ENV: JSON.stringify("dev"),    }),  ],  devServer: {    compress: true,    client: { progress: true },    port: 5000,    // mock数据    setupMiddlewares: (middlewares, devServer) => {      if (!devServer) {        throw new Error("webpack-dev-server is not defined");      }      middlewares.unshift({        name: "fist-in-array",        // `path` 是可选的        path: "/user",        middleware: (req, res) => {          res.send({ name: "moon mock" });        },      });      return middlewares;    },  },});

应用环境变量后目录构造大抵如下

├─.eslintrc.json├─package-lock.json├─package.json├─postcss.config.js├─webpack.config.base.js├─webpack.config.dev.js├─webpack.config.prod.js├─src|  ├─index.html|  ├─js|  | ├─index.js|  | ├─server.js|  | └test.js|  ├─image|  |   └logo.png|  ├─css|  |  ├─a.css|  |  └index.css├─doc|  └notes.md├─dist

更改配置文件后,打包命令也要做适当调整,打包时须要指定配置文件:

// 开发模式webpack --config webpack.config.dev.js// 生产模式webpack --config webpack.config.prod.js

生产模式配置文件和公共配置文件源码前期上传

热更新

webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。这个机制能够做到不必刷新浏览器而将新变更的模块替换掉旧的模块。默认启用热更新,无需配置,它会主动利用 webpack.HotModuleReplacementPlugin,这是启用 HMR 所必须的。

优化

上面的配置代码都是在 webpack 配置文件中,不再赘述

放大构建范畴

include/exclude 选其一即可

module: {    rules: [        {            test: /\.js$/,            include: path.resolve(__dirname, "src"),            // exclude: /node_modules/,        },    ]}

module.noParse

因为webpack会通过入口文件解析 import, require 援用的包,还会去剖析包的依赖,但有些包是没有依赖的,因而能够通过 noParse 不解析某个援用包中的依赖关系,来进步构建性能。适宜没有依赖项的包,如 jquery

module: {    noParse: /jquery/,}

webpack.IgnorePlugin

webpack 内置插件 IgnorePlugin 能够阻止生成用于导入的模块,或要求调用与正则表达式或筛选函数匹配的模块。如 moment 包内引入了很多语言包,这些语言包都放在 locale 文件夹下,但大部分理论场景只会援用一个的语言包,因而打包时可疏忽 moment 目录下的 locale 语言包

 new webpack.IgnorePlugin({  resourceRegExp: /^\.\/locale$/,  contextRegExp: /moment$/,}), 

疏忽后再从新再js文件中引入某个语言包就能失常应用了

import "moment/locale/zh-cn";moment.locale("zh-cn");

抽离公共代码

个别用在多页利用场景或者是单个 js 文件太大,申请须要很长时间,须要拆成几个js文件,优化申请速度,应用 optimization 的 splitChunks 属性来优化。

splitChunks.cacheGroups 缓存组能够继承和/或笼罩来自 splitChunks.* 的任何选项。然而 testpriorityreuseExistingChunk 只能在缓存组级别上进行配置。将它们设置为 false以禁用任何默认缓存组。

看上面配置之前先理解splitChunks的几个属性:

  • priority:抽离代码的优先级,值越高越先被抽离,避免某些模块在后面的模块抽离完了前面没被抽离到,在本例中是避免 vendor 模块被 common 模块抽离完了没被抽离到
  • name:每个模块(chunk)的文件名,不定义将是随机名字
  • test:匹配目录
  • chunks:抉择哪些 chunk 进行优化

    • initial:从入口处开始提取代码,若有异步模块思考前面两个值
    • async:异步模块
    • all:能够存在异步和非异步模块
  • minSize:生成 chunk 的最小体积,此处为不便测试设置为 0
  • minChunks:拆分前必须共享模块的最小 chunks 数,以后代码块援用多少次才被抽离,此处为不便设置设置为 1

本例中宰割了 common 和 vendor 两个 chunk

optimization: {    // 宰割代码块    splitChunks: {      // 缓存组      cacheGroups: {        // 公共模块        commons: {          name: "common",          chunks: "initial",           minSize: 0,           minChunks: 1,         },        vendor: {          name: "vendor",          priority: 1,           test: /[\\/]node_modules[\\/]/,          chunks: "all", //包含异步和非异步代码块        },      },    },  },

为不便大家了解,献上打包后的目录树结构

├─index.html├─js| ├─common.js| ├─common.js.LICENSE.txt| ├─main.js| ├─main.js.LICENSE.txt| ├─vendor.js| └vendor.js.LICENSE.txt├─images|   └logo.png├─doc|  └notes.md├─css|  └main.css
这一块比拟难了解,倡议多试几次打包比照差别就懂了

懒加载

通过 es6 的 import() 语法实现懒加载,通过 jsonp 实现动静加载文件,import 函数返回的是 promise 对象。vue 懒加载,react 懒加载都是这样实现的。举个简略的栗子,某些 js 文件在按钮点击后再申请加载。

此处省略获取 button dom元素对象的代码button.addEventListener('click', function(){    import('./test.js').then(data => {        console.log(data);    })})
除了以上的优化办法之外,还有dll预构建,多线程构建/压缩,利用缓存晋升二次构建速度,动静 polyfill 等等,可依据理论状况自行抉择优化计划,这里不一一赘述

webpack自带优化

tree-shaking

应用 import 语法在生产环境下没用到的代码不会被打包, 即 tree shaking, require 语法不反对tree-shaking

scope hosting

scope hosting(作用域晋升),举个栗子:

let a = 1let b = 2let c = 3let d = a+b+cconsole.log(d)

代码打包进去只有最初一句, webpack打包会主动省略一些能够简化的代码

手写繁难less-loader

less-loader.js

/loaders/less-loader.js 目录文件中引入 less 插件

const less = require("less");function loader(source) {  let css = "";  less.render(source, function (err, res) {    css = res.css;  });}module.exports = loader;

webpack.config.js

写入以下配置

resolveLoader: {    alias: {        "lessLoader": path.resolve(__dirname, "loaders", "less-loader"))    }},module: {    rules: [        {            test: /\.less/,            use: ["style-loader", "lessLoader"]        }    ]}