2021-04-17 Webpack工程化实战

267 阅读8分钟

创建项目

mkdir webpack-project
cd webpack-project
npm init -y

项目准备

安装webpack@4.43.0和webpack-cli@3.3.12版本依赖存放到开发环境下

yarn add webpack@4.43.0 webpack-cli@3.3.12 -D

新建.npmrc文件指定安装源registry="https://registry.npm.taobao.org/",这样就不用每次安装项目的时候切换源了

新建webpack.config.js配置项

module.exports = {
    entry, // 入口 string || array || object
    output, // 出口 object {path, filename}
    mode, // 打包模式 string
    module, // 各种处理chunk的loader object {rules:[{test, use: string||array||array[object{loader,options}]}]}
    plugins // 插件
}

构建webpack,在package.json文件中scripts字段中写入脚本dev:'webpack'打包构建输出bundle文件

兼容各种浏览器前缀css样式,需要在package.json文件中新建browserslist字段为数组例如["last 2 version", "> 1%"]

样式处理

1、css文件处理

需要两个loader:css-loader、style-loader

# 安装:
yarn add css-loader style-loader -D

# 配置
module: {
    rules: [
        {
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
        }
    ]
}

loader的处理顺序为从后往前,如上述rules中的loader先处理css-loader把css文件转为序列化(string)chunk,然后再由style-loader处理创建style标签把代码块放进去,这样的话样式就生效了

2、同样less、sass文件处理也是一样的步骤

- less需要三个loader:style-loader、css-loader、less-loader,需要额外安装yarn add less less-loader@7.3.0 -D
- scss也需要三个loader:style-loader、css-loader、sass-loader,需要额外安装yarn add node-sass sass-loader -D

3、postcss至于css相当于babel至于js,跟webpack一样是工具,其功能一是把css处理成js可以操作的抽象语法树AST,其二就是调用插件来处理AST并得到结果

- 自动浏览器前缀:autoprefixer
- css压缩等cssnano
- 根目录下创建postcss.config.js文件,写入配置,使用方式和less-loader一样
module.exports = {
  plugins: [require("autoprefixer"), require("cssnano")]
};

4、样式文件分离

经过loader的处理,css最后会被打包到js中,运行时会动态的插入head中,但是一般在生产环境中会把css文件分离出来(有利于客户端缓存、并行加载及减少js包的大小),这时候用到插件mini-css-extract-plugin,会以link的方式插入到html中

const minicss = require("mini-css-extract-plugin");

module.exports = {
    ...
    module:{
        rules:[
            {
            test: /\.less$/,
            use: [
              //   "style-loader", // 创建style标签方式
              minicss.loader,  // 以link方式插入html中
              "css-loader",
               ]
             }
           ]
      },
    plugins: [
        new minicss({
            filename: "[name].css"
        })
    ]
}

5、图片字体文件处理

file-loader和url-loader都可以用来处理本地的资源文件,如图片、字体、音频等。不过url-loader可以指定文件的大小小于限定一般为1024*33KB,转为base64URL,不会输出真实的文件,可以减少网络请求

use: [
    {
        loader: 'url-loader', // 配置url-loader即可,内部会自动调用file-loader
        options: {
            name: '[name].[ext]', //输出名字占位符[name],[contenthash],[chunkhash]等
            limit: 1024*3, // 一般3KB以下图片转为base64URL
            ...
        }
    }
]

使用图片的时候,file-loader有outputPath和publicPath两个参数极为重要

module.exports = {
    ...
    module: {
        rules: [
            {
                test: /\.(png|jpe?g|gif|webp)$/,
                use: {
                  loader: "file-loader",
                  options: {
                    name: "[name].[ext]",
                    outputPath: "images/", // 图片的输出位置,也就是存放位置
                    publicPath: "../images/" // 图片的引用位置,css中如何引用图片
                    // 图片的路径+图片的名称 ../images/ + logo.png
                  }
                }
              },
        ]
    }
}

devtool配置选项

devtool选项为cheap-module-eval-source-map开启开发模式的代码映射,帮助更快的查找错误来源位置

devServer配置选项

devServer选项设置跟webpack-dev-server开启本地服务有关

yarn add webpack-dev-server@3.11.0 -D
devServer: {
    contentBase: path.join(__dirname, "dist"), // 表示服务器从哪个目录去查找内容文件(即页面文件,比如HTML)
    port: 8081, // 端口
    open: true, // 自动打开浏览器
    proxy: { // 代理
      "/api": {
        target: "http://localhost:9092/"
      }
    }
}

当开启webpack-dev-server时,会自动接管webpack构建,并且会把打包文件保存到本地内存中,这样读取更快,修改文件保存时会浏览器会自动刷新——热更新

HMR:热模块替换(Hot Module Replacement)

当我们修改文件的时候,浏览器不刷新,内容自动变化,这就是热更新

  • CSS模块HMR
devServer: {
    contentBase: path.join(__dirname, "dist"), 
    port: 8081, // 端口
    open: true, // 自动打开浏览器
    hot: true, // 开启热替换-CSS
}

注意不支持chunkhashcontenthash

  • JS模块HMR-HotModuleReplacementPlugin插件
devServer: {
    contentBase: path.join(__dirname, "dist"), 
    port: 8081, // 端口
    open: true, // 自动打开浏览器
    hot: true, // 开启热替换
    hotOnly: true, // 关闭浏览器的自动刷新
}

注意:需要使⽤module.hot.accept来观察模块更新,本质上就是监控目录下文件,先删除文件再生成文件这样就不影响其他模块

插件优化

1、自动生成html文件,html-webpack-plugin会在打包结束后,⾃动⽣成⼀个html⽂件,并把打包⽣成的js模块引⼊到该html,一般使用两个配置项template和filename 中

yarn add html-webpack-plugin@4 -D
const path = require('path')
const htmlwebpackplugin = require('html-webpack-plugin')

module.exports = {
    ...
    plugins: [
        new htmlwebpackplugin({
            template: './src/index.html',
            filename: '[name].html'
        })
    ]
}

2、自动清除打包文件clean-webpack-plugin

yarn add clean-webpack-plugin -D
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
...
plugins: [
 new CleanWebpackPlugin()
]

如何编写一个loader

  • loader本质上就是一个函数,不可以是箭头函数

      这是因为要使用到上下文的this,调用callback等API,必须是一个声明式函数,该函数接收一个参数是源码
    
  • loader必须有返回值,string或buffer

  • 参数是source源代码,对其进行加工处理返回源码

  • this.callback API返回多个信息

  • this.async API处理异步逻辑

  • this query API 处理配置文件中options参数

案例1-创建一个替换源码中字符串的loader

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

// src/myLoader/replace-loader.js
module.exports = function (source){
    return source.replace('webpack', 'kkb')
}

在配置文件中使用自定义loader

module.exports = {
    ...
    // 这个选项是解析除了node_modules中其他文件夹,这样就不用每次手动path.resolve路径了,只需要写名字即可
    resolveLoader: {
        modules: ["node_modules", "./myLoaders"]
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    loader: 'replace-loader',
                    options: { // 这个选项传参给this.query API
                        name: '开课吧'
                    }
                ]
            }
        ]
    }
}

指纹策略

webpack中有三种指纹策略,分别是hashchunkhashcontenthash

  • hash:作用范围最大,内容有更新就会发生变化,一般取6位[hash:6]

    注意:整个工程不管单页面入口还是多页面入口,只要有文件内容更新hash就会发生变化

  • chunkhash:作用范围是一个chunks,在多页面入口中常用到,也是一般取6位[chunkhash:6]

    注意:同属一个chunks下的css文件生成打包文件也是chunkhash命名,css更新整个chunks下的hash都变化

  • contenthash:自身内容更新,hash才会更新,一般取6位[contenthash:6]

    注意:例如index.js中有index.css,css命名[contenthash:6],js命名[chunkhash:6],则css改变,js中hash变化;反之,js改变,css中hash不变

总结:单页面用hash,多页面用chunkhash,资源文件用contenthash

index chunks
    index.js chunk   [chunkhash:6]
        index.css [contenthash:6]
list chunks [hash:6]  
    list.js chunk 

多页面打包方案

两个问题:一是如何动态生成entry;二是如何遍历entry对象,动态实例化htmlwebpackplugin

文件目录如下:

image.png

根据格式,每个目录下都有index.js文件就默认为一个入口目录

完整代码:

/**
 * 多页面打包方案
 * 多入口entry
 *
 * key chunks
 * 循环遍历对象,动态的实例化htmlwebpackplugin
 */

/**
 * webpack配置项
 */

const path = require("path");
const htmlwebpackplugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const minicss = require("mini-css-extract-plugin");

const glob = require("glob");
const setMPA = () => {
  const entry = {};
  const htmlwebpackplugins = [];
  // 页面路径 glob通配符
  const entryFiles = glob.sync(path.join(__dirname, "./src/*/index.js"));
  //   console.log(entryFiles);
  entryFiles.forEach((item, index) => {
    const entryFile = item;
    const match = entryFile.match(/src\/(.*)\/index\.js/);
    // console.log(match);
    const pageName = match[1];
    entry[pageName] = item;
    htmlwebpackplugins.push(
      new htmlwebpackplugin({
        template: path.join(__dirname, `src/${pageName}/index.html`),
        filename: `${pageName}.html`,
        chunks: [pageName]
      })
    );
  });

  //返回entry和htmlwebpackplugins
  return {
    entry,
    htmlwebpackplugins
  };
};

const { entry, htmlwebpackplugins } = setMPA();

module.exports = {
  entry,
  output: {
    path: path.resolve(__dirname + "/mpa"),
    filename: "[name]-[chunkhash:6].js"
  },
  mode: "development",
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|webp)$/,
        use: {
          // loader: "file-loader",
          loader: "url-loader", // url-loader就是file-loader的变体,内部会自动调用file-loader
          options: {
            name: "[name].[ext]",
            outputPath: "images/", // 图片的输出位置,也就是存放位置
            publicPath: "../images/", // 图片的引用位置,css中如何引用图片
            // 图片的路径+图片的名称 ../images/ + logo.png
            limit: 1024 * 3 // 3KB,小于3kb使用url-loader,否则使用file-loader
          }
        }
      },
      {
        test: /\.woff2$/,
        use: {
          loader: "file-loader",
          options: {
            name: "[name].[ext]",
            outputPath: "css/",
            publicPath: "./"
          }
        }
      },
      {
        test: /\.less$/,
        use: [minicss.loader, "css-loader", "less-loader"]
      }
    ]
  },
  plugins: [
    new minicss({
      filename: "css/[name]-[contenthash:6].css"
    }),
    new CleanWebpackPlugin(),
    ...htmlwebpackplugins
  ]
};

babel处理js模块

babel是Javascript编译器,能够将ES6代码转换成ES5代码,并且可以兼容各个浏览器

babel在执行编译的过程中,首先会从项目的根目录下的.babelrcJSON文件中读取配置,没有会从配置文件中的loader的options选项中读取配置

  • 兼容低版本浏览器
  • 支持其他技术栈,如Vue,React,TypeScript
  • ES6+语法如const、let
  • ES6新特性如promise

babel就是个工具,不干活,干活的是preset(即一组预先设定的插件)选项有以下四种:

  • @babel/preset-env:把ES6+语法转为ES5
  • @babel/prest-flow:处理flow语法
  • @babel/preset-react:处理JSX
  • @babel/preset-typescript:处理TypeScript

安装

yarn add @babel/core @babel/preset-env babel-loader -D

babel-loader是webpack与babel的通信桥梁,不会做把ES6转成ES5的工作,这部分工作需要用到@babel/preset-env来做

// webpack.config.js

{
    test:/\.js$/,
    exclude: /node_modules/,
    use: {
        loader: "babel-loader",
        options: {
            presets: ["@babel/preset-env"]
        }
    }
}

通过上面的几步还不够,默认的Babel只支持let、const等一些基础的ES6语法转换,如Promise等新特性转换则需要借助@babel/polyfill,把ES6+的新特性都装过来,来弥补低版本浏览器中缺失的特性

@babel/polyfill

yarn add @babel/polyfill -S // 需要安装到生产环境中

按需加载,减少冗余

index.js中第一行加入import "@babel/polyfill"会发现打包后的体积大很多,这是因为polyfill默认把所有的特性注入进来

实现按需加载的方式如下:

// webpack.config.js

options: {
    presets: [
        ["@babel/preset-env",
        {
            targets: { // 新版本浏览器都支持ES6+新特性
                edge: "17",
                firefox: "60",
                chorme: "67"
            },
            corejs: 2, // 新版本需要指定核心版本库 3x只是比2多包含新特性数量
            useBuiltIns: "usage" // 按需引入
        }]
    ]
}

useBuiltIns选项是babel7的新功能,这个选项有三个参数可用:①entry: 需要在 webpack 的⼊⼝⽂件⾥ import "@babel/polyfill" ⼀次。 babel会根据你的使⽤情况导⼊垫⽚,没有使⽤的功能不会被导⼊相应的垫⽚。 ②usage: 不需要 import ,全⾃动检测,但是要安装 @babel/polyfill 。 ③false: 如果你 import "@babel/polyfill" ,它不会排除掉没有使⽤的垫⽚,程序体积会庞⼤。(不推荐)

如何编写一个plugin

webpack在编译代码过程中有生命周期,每个周期对应不同的打包阶段module和assets

plugin本质上是一个class类,实现emit阶段资源列表里生成一个txt文档的插件

class textwebpackplugin {
  constructor(options) {}

  // 注册钩子
  apply(compiler) {
    compiler.hooks.emit.tapAsync("textwebpackplugin", (compliation, cb) => {
      compliation.assets["new.txt"] = {
        source: function () {
          // 资源的内容
          return "hello 自定义plugins";
        },
        size: function () {
          // 资源的大小
          return 1024;
        }
      };
      cb();
    });
  }
}

module.exports = textwebpackplugin;

webpack的打包流程

  1. 拿到配置,初始化工作,最终配置
  2. 实例化一个compile类,注册插件,对应的生命周期绑定相应的事件
  3. 执行编译,compile.run
  4. compile(构建阶段)->compilation(bundle资源被加工成什么样子)
  5. 递归处理所有的依赖模块生成chunk
  6. 把chunk输出到output指定的位置

打包公共库

如果我们打包的目的是生成一个供别人使用的库,开源的,那么就需要用到output.library来指定公共库的名称

output.libraryTarget指定打包库的规范,通常有var、this、commonjs、umd等等

output.libraryExport指定导出默认如函数或者变量等

module.exports = {
    output: {
        library: 'myLib', // 支持占位符[name]
        libraryTarget: 'umd', // 指定规范
        libraryExport: "default" // 指定导出
    }
}

性能优化

优化的目的:

  • 优化开发体验
  • 优化输出质量

影响webpack构建速度的有两个大户:一是loader和plugin方面的构建过程;二是压缩,要优化构建过程,可以从减少查找过程多线程提前编译Cache缓存多个角度来优化

减少查找过程

1、优化loader查找范围

可以从test include exclude三个配置项来缩小loader的处理范围

// 推荐使用include
// string
include: path.resolve(__dirname, "./src"),

// array
include: [
    path.resolve(__dirname, 'app/styles'),
    path.resolve(__dirname, 'vendor/styles')
]

注意:exclude优先级要高于include和test

2、优化resolve.modules配置

用于配置webpack去哪些目录下寻找第三方模块,默认是['node_modules'],在当前目录下的node_modules里去找,没有往上级的node_modules里找,递归处理

如果我们第三方模块都安装在项目根目录下,可以指定这个路径

module.exports = {
    resolve: {
        modules: [path.resolve(__dirname, "./node_modules")]
    }
}

3、优化resolve.alias配置

通过别名来将原导入路径映射成一个新的导入路径,直接指定文件,避免耗时

module.exports = {
    resolve: {
        alias: {
            "@": path.join(__dirname, "./src")
        }
    }
}
多线程

由于运行在Node.js之上的Webpack是单线程模型的,所以Webpack需要处理的事情要一件一件做,不能多件事一起做,我们需要Webpack能同一时间处理多个任务,发挥多核CPU电脑的威力

thread-loader是针对loader进行优化的,会将loader放置在一个worker池里进行,以达到多线程构建

module.exports = {
 module: {
     rules: [
         {
             test: /\.js$/,
             include: path.resolve('src'),
             use: [
                 'thread-loader',
                 // 你的高开销的loader放置在此如babel-loader
             ]
         }
     ] 
 }
}
Cache缓存

提升构建速度的另一个大杀器就是使用缓存

Webpack中打包的核心是Javascript文件的打包,使用的babel-loader,其实很多时候打包时间长都是babel-loader执行慢导致的,其会产生一些运行期间重复的公共文件,造成代码体积的冗余,同时也会减慢编译的速度

babel-loader提供cacheDirectory配置给Babel编译设定给定的缓存目录

rules: [
    {
        test: /\.js$/,
        loader: 'babel-loader',
        options: {
            cacheDirectory: true
        }
    }
]
压缩速度的优化

相当于构建过程而言,压缩只有生产环境打包时才会做,通过terser-webpack-plugin开启多线程和缓存

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    optimization: {
        minimizer: [
            new TerserPlugin({
                cache: true, // 开启缓存
                parallel: true // 多线程
            })
        ]
    }
}
压缩体积的优化

主要分为以下几个方面:

1、css压缩,使用optimize-css-assets-webpack-plugin和cssnano

在Webpack中,css-loader已经集成了cssnano,我们还可以使用optimize-css-assets-webpack-plugin来自定义cssnano的规则

const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

new OptimizeCSSAssetsPlugin({
 cssProcessor: require("cssnano"), // 这⾥制定了引擎,不指定默认也是 cssnano
 cssProcessorOptions: {
     discardComments: { removeAll: true }
 }
})

2、html压缩

使用html-webpack-plugin

new htmlwebpackplugin({
      title: "Webpack React",
      template: "./src/index.html",
      minify: {
        removeComments: true, // 移除HTML中的注释
        collapseWhitespace: true, // 删除空白符与换行符
        minifyCSS: true // 压缩内联css
      }
    })

3、js压缩

mode=production下,Webpack会自动压缩代码,也可以使用terser-webpack-plugin自定义压缩

tree-shaking:擦除无用的js和css代码

tree shaking的原理是利用ES模块化引入静态分析流程,在编译时就正确知道加载了哪些模块,从而判断哪些模块未被加载或变量未被引用,再进行删除即可

摇树,清除无用的CSS和JS(Dead Code)

  • 代码不会被执行,不可到达

  • 代码执行的结果不会被用到

  • 代码只会影响死变量(只写不读)

  • JS只支持import和export方式!!!

    清除无用的css方式:需要安装glob-all purify-css purifycss-webpack
    清除无用的js方式:开启optimization.usedExports为true
    

code-splitting:代码分割

optimization: {
    splitChunks: {
        chunks: 'all' // 所有的chunks代码公共部分分离出来称为一个单独的文件
    }
}

HardSourceWebpackPlugin 使用HardSourceWebpackPlugin缩短构建时间,其是一个非常强大的插件,可以大大的缩短构建时间,webpack5已经内置插件

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')

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