对webpack的理解

记录自己对webpack的理解
在这里插入图片描述

webpack的配置详解

const path = require('path');
module.exports = {
  entry: "./app/entry", // string | object | array
  // Webpack打包的入口
  output: {  // 定义webpack如何输出的选项
    path: path.resolve(__dirname, "dist"), // string
    // 所有输出文件的目标路径
    filename: "[chunkhash].js", // string
    // 「入口(entry chunk)」文件命名模版
    publicPath: "/assets/", // string
    // 构建文件的输出目录
    /* 其它高级配置 */
  },
  module: {  // 模块相关配置
    rules: [ // 配置模块loaders,解析规则
      {
        test: /.jsx?$/,  // RegExp | string
        include: [ // 和test一样,必须匹配选项
          path.resolve(__dirname, "app")
        ],
        exclude: [ // 必不匹配选项(优先级高于test和include)
          path.resolve(__dirname, "app/demo-files")
        ],
        loader: "babel-loader", // 模块上下文解析
        options: { // loader的可选项
          presets: ["es2015"]
        },
      },
  },
  resolve: { //  解析模块的可选项
    modules: [ // 模块的查找目录
      "node_modules",
      path.resolve(__dirname, "app")
    ],
    extensions: [".js", ".json", ".jsx", ".css"], // 用到的文件的扩展
    alias: { // 模块别名列表
      "module": "new-module"
	  },
  },
  devtool: "source-map", // enum
  // 为浏览器开发者工具添加元数据增强调试
  plugins: [ // 附加插件列表
    // ...
  ],
}
语法解析:babel-loader

样式解析:style-loader

css解析:css-loader

less解析:less-loader

文件解析:url-loader(file-loalder)

性能分析:webpack-bundle-analyzer


webpack是什么

Webpack 启动后会从配置的 Entry 出发,解析出文件中的导入语句,再递归的解析。

在遇到导入语句时 Webpack 会做两件事情:

1.根据导入语句去寻找对应的要导入的文件。例如 require('react') 导入语句对应的文件是 ./node_modules/react/react.js , require('./util') 对应的文件是 ./util.js 。
2.根据找到的要导入文件的后缀,使用配置中的 Loader 去处理文件。例如使用 ES6 开发的 JavaScript 文件需要使用 babel-loader 去处理。

loader

webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。
在这里插入图片描述

一、webpack优化

从项目自身出发

我们的项目是将js分离,不同页面加载不同的js。然而分析webpack打包过程并针对性提出优化方案是一个比较繁琐的过程,首先我们需要知道webpack 打包的流程,从而找出时间消耗比较长的步骤,进而逐步进行优化。

在优化前,我们需要找出性能瓶颈在哪,代码组织是否合理,优化相关配置,从而提升webpack构建速度。

1.使用yarn而不是npm

由于项目使用npm安装包,容易导致在多关联依赖关系中,很可能某个库在指定依赖时没有指定版本号,进而导致不同设备上拉到的package版本不一。yarn不管安装顺序如何,相同的依赖关系将以相同的方式安装在任何机器上。当关联依赖中包括对某个软件包的重复引用,在实际安装时将尽量避免重复的创建。yarn不仅可以缓存它安装过的包,而且安装速度快,使用yarn无疑可以很大程度改善工作流和工作效率
2.删除没有使用的依赖
比如我们在开发的过程中,引入了一个模块,但是并没有使用到,在业务中并没有使用到a 模块,但webpack 会针对该import 进行打包一遍,这无疑造成了性能的浪费。
1.打包过程分析

我们知道,webpack 在打包过程中会针对不同的资源类型使用不同的loader处理,比如sass,ts,es6等资源,然后将所有静态资源整合到一个bundle里,以实现所有静态资源的加载。webpack最初的主要目的是在浏览器端复用符合CommonJS规范的代码模块,而CommonJS模块每次修改都需要重新构建(rebuild)后才能在浏览器端使用。
webpack的打包过程

对于单入口文件,每个入口文件把自己所依赖的资源全部打包到一起,即使一个资源循环加载的话,也只会打包一份
对于多入口文件的情况,分别独立执行单个入口的情况,每个入口文件各不相干

如何定位webpack打包速度慢的原因

1.减小打包文件体积

2.代码压缩

3.happypack

此外,happypack同时还利用缓存来使得rebuild 更快

(1)由于运行在 Node.js 之上的 Webpack 是单线程模型的,所以 Webpack
需要处理的事情只能一件一件地做,不能多件事一起做。

(2)而 HappyPack 的处理思路是:将原有的 webpack 对 loader
的执行过程,从单一进程的形式扩展多进程模式,从而加速代码构建。

4.缓存与增量构建

5.减少构建搜索或编译路径

6.webpack配置别名减少文件搜索范围

设置resolve.alias字段,避免打包时如果使用相对路径访问或着import文件时会层层去查找解析文件。

在使用实际项目开发中,为了提升开发效率,很明显你会使用很多成熟第三方库;即便自己写的代码,模块间相互引用,为了方便也会使用相对路劲,或者别名(alias);这中间如果能使得
Webpack 更快寻找到目标,将对打包速度产生很是积极的影响。于此,我们需要做的即:减小文件搜索范围,从而提升速度

优化 loader 配置

由于 Loader 对文件的转换操作很耗时,需要让尽可能少的文件被 Loader 处理。
在 2-3 Module 中介绍过在使用 Loader 时可以通过 test 、 include 、 exclude 三个配置项来命中 Loader 要应用规则的文件。

为了尽可能少的让文件被 Loader 处理,可以通过 include 去命中只有哪些文件需要被处理。以采用 ES6 的项目为例,在配置 babel-loader 时,可以这样:

module.exports = {
 module: {
  rules: [
   {
    // 如果项目源码中只有 js 文件就不要写成 /\.jsx?$/,提升正则表达式性能
    test: /\.js$/,
    // babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
    use: ['babel-loader?cacheDirectory'],
    // 只对项目根目录下的 src 目录中的文件采用 babel-loader
    include: path.resolve(__dirname, 'src'),
   },
  ]
 },
};

可以适当的调整项目的目录结构,以方便在配置 Loader 时通过 include 去缩小命中范围。

优化 resolve.modules 配置

在 2-4 Resolve 中介绍过 resolve.modules 用于配置 Webpack 去哪些目录下寻找第三方模块。

resolve.modules 的默认值是 [‘node_modules’] ,含义是先去当前目录下的 ./node_modules 目录下去找想找的模块,如果没找到就去上一级目录 …/node_modules 中找,再没有就去 …/…/node_modules 中找,以此类推,这和 Node.js 的模块寻找机制很相似。

当安装的第三方模块都放在项目根目录下的 ./node_modules 目录下时,**没有必要按照默认的方式去一层层的寻找,可以指明存放第三方模块的绝对路径,以减少寻找,**配置如下:

module.exports = {
 resolve: {
  // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
  // 其中 __dirname 表示当前工作目录,也就是项目根目录
  modules: [path.resolve(__dirname, 'node_modules')]
 },
};

优化 resolve.mainFields 配置

在 2-4 Resolve 中介绍过 resolve.mainFields 用于配置第三方模块使用哪个入口文件。
为了减少搜索步骤,在你明确第三方模块的入口文件描述字段时,你可以把它设置的尽量少

优化 resolve.extensions 配置

在导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试询问文件是否存在。

在 2-4 Resolve 中介绍过 resolve.extensions 用于配置在尝试过程中用到的后缀列表,默认是:

extensions: ['.js', '.json']

也就是说当遇到 require(’./data’) 这样的导入语句时,Webpack 会先去寻找 ./data.js 文件,如果该文件不存在就去寻找 ./data.json 文件,如果还是找不到就报错。

如果这个列表越长,或者正确的后缀在越后面,就会造成尝试的次数越多,所以 resolve.extensions 的配置也会影响到构建的性能。
在这里插入图片描述
以上都是缩小文件的方式。

7. vue-router路由懒加载

 懒加载:也叫延迟加载,即在需要的时候进行加载,随用随载。
 使用懒加载的原因:  vue是单页面应用,使用webpcak打包后的文件很大,会使进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。运用懒加载后,就可以按需加载页面,提高用户体验。

懒加载的写法:

8.打包后的js过大,将js打包多个文件

9.去掉不必要的插件

按需引入,比如element-ui

10.开启gzip和缓存

web前端项目,静态资源放在cdn上比较多,gzip的压缩是非常必要的,它直接改变了js文件的大小,减少两到三倍。
在这里插入图片描述

11.缓存loader的执行结果(cacheDirectory)

cacheDirectory是loader的一个特定的选项,默认值是false。
在这里插入图片描述

12.DllPlugin分包

13使用CDN

将vue,vue-router放到CDN上


引入cdn连接
在webpack设置中添加externals
删除或注释项目中相应资源的引入(import)

在项目中可以看到资源是由不同线程同时加载的

在这里插入图片描述

二、webpck的插件原理

在 webpack 中,专注于处理 webpack 在编译过程中的某个特定的任务的功能模块,可以称为插件。它和 loader 有以下区别:

  1. loader 是一个转换器,将 A 文件进行编译成 B 文件,比如:将 A.less 转换为
    A.css,单纯的文件转换过程。webpack 自身只支持 js 和 json 这两种格式的文件,对于其他文件需要通过 loader将其转换为 commonJS 规范的文件后,webpack 才能解析到。

  2. plugin 是一个扩展器,它丰富了 webpack 本身,针对是 loader 结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些节点,执行广泛的任务。

plugin 的特征

是一个独立的模块。

模块对外暴露一个 js 函数。

函数的原型 (prototype) 上定义了一个注入 compiler 对象的 apply 方法。

apply 函数中需要有通过 compiler 对象挂载的 webpack 事件钩子,钩子的回调中能拿到当前编译的 compilation
对象,如果是异步编译插件的话可以拿到回调 callback。

完成自定义子编译流程并处理 complition 对象的内部数据。

如果异步编译插件的话,数据处理完成后执行 callback 回调。

class HelloPlugin {
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options) {}
  // Webpack 会调用 HelloPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler) {
    // 在emit阶段插入钩子函数,用于特定时机处理额外的逻辑;
    compiler.hooks.emit.tap('HelloPlugin', (compilation) => {
      // 在功能流程完成后可以调用 webpack 提供的回调函数;
    })
    // 如果事件是异步的,会带两个参数,第二个参数为回调函数,
    compiler.plugin('emit', function (compilation, callback) {
      // 处理完毕后执行 callback 以通知 Webpack
      // 如果不执行 callback,运行流程将会一直卡在这不往下执行
      callback()
    })
  }
}
module.exports = HelloPlugin

1.webpack 读取配置的过程中会先执行 new HelloPlugin(options) 初始化一个 HelloPlugin 获得其实例

2.初始化 compiler 对象后调用 HelloPlugin.apply(compiler) 给插件实例传入 compiler 对象。

3.插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin (事件名称, 回调函数) 监听到 Webpack 广播出来的事件。并且可以通过 compiler 对象去操作 Webpack。

事件流机制

webpack 本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 Tapable

Webpack 的 Tapable 事件流机制保证了插件的有序性,将各个插件串联起来, Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条 webapck 机制中,去改变 webapck 的运作,使得整个系统扩展性良好。

Tapable 也是一个小型的 library,是 Webpack 的一个核心工具。类似于 node 中的 events 库,核心原理就是一个订阅发布模式。作用是提供类似的插件接口

常用的插件有哪些

1.热模块替换Hot Module Replacement

热模块替换(HMR)是webpack提供的最有用的特性之一,热模块替换可以让模块在没有页面刷新的情况下实时更新代码改动结果;
使用方法:
安装dev-serve
webpack-dev-server 是一个小型的Node.js Express服务器,它通过使用webpack-dev-middleware来为webpack打包的资源文件提供服务。可以认为webpack-dev-server就是一个拥有实时重载能力的静态资源服务器(建议只在开发环境使用)

npm install webpack-dev-server --save-dev```
运行

// 引入插件
  plugins: [
    new webpack.HotModuleReplacementPlugin() // Enable HMR
  ],
  // devServe配置hot:true开启模块热更新配置
   devServer: {
    hot: true, // Tell the dev-server we're using HMR
    contentBase: resolve(__dirname, 'dist'),
    publicPath: '/'
  }

inline属性用于切换webpack-der-server编译并刷新浏览器的两种不同模式:
(1)第一种也是默认的inline模式,这种模式是将脚本插入打包资源文件中复制热更新,并在浏览器控制台中输出过程信息,访问格式访问格式是http://😕 ;
(2)iframe 模式:使用iframe加载页面,访问格式http://:/webpack-dev-server/
可以通过配置

inline: false//启用iframe
inline:true // inline模式

在你的代码中插入热替换代码
index.js 在入口文件结尾处插入

if (module.hot) {
    module.hot.accept();
}
只有被 "accept"的代码模块才会被热更新,所以你需要在父节点或者父节点的父节点…

2.ProvidePlugin

ProvidePlugin 可以在任何地方自动加载模块而不需要import 或 require 方法:

//Webpack plugins定义
new webpack.ProvidePlugin({
  $: 'jquery', // 当调用$时自动加载jquery模块
  jQuery: 'jquery'
})

// 代码模块中调用
$('#item'); // <= 生效
jQuery('#item'); // <= just works
// $ is automatically set to the exports of module "jquery"

ProvidePlugin还可以根据不同环境使用不同配置

//development.js开发
module.exports = {
    name:'development'
};
//test.js测试
module.exports = {
    name:'test'
};

//production.js线上
module.exports = {
    name:'production'
};

//webpack.dev.config.js 开发环境
    new webpack.ProvidePlugin({
            ENV: 'development'
        })
//webpack.test.config.js 测试环境
new webpack.ProvidePlugin({
            ENV: "test"
        })
//webpack.pub.config.js 线上环境
    new webpack.ProvidePlugin({
            ENV: "production"
        })

3.CommonsChunkPlugin

webpack命中缓存,在webpack4中废弃了CommonsChunkPlugin,他有一个默认的额分包策略。
提取第三方库
在通常的项目开发中我们通常会引入一些第三方库,在打包的时候我们通常也会希望将代码拆分成公共代码和应用代码。将webpack.dev.config.js文件配置变化如下:

//webpack.dev.config.js
var path = require('path')
var webpack = require('webpack')

module.exports = {
  entry: {
    main: './main.js',
    main1: './main1.js',
    lib:['./lib/jquery.js','./lib/vue.js']//第三方库
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
    chunkFilename: '[name].js',
  },
  resolve: {
    extensions: [' ', '*', '.js', '.jsx'],
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      names:['commons','lib']//'lib'提取入口entry key 'lib'代表的文件单独打包
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'load'
    })
  ],
}

持久化缓存其实是一个老生常谈的问题,前端发展到现在,缓存方案已经很成熟了。简单原理:

1.针对 html 文件:不开启缓存,把 html 放到自己的服务器上,关闭服务器的缓存
2.针对静态的 js,css,图片等文件:开启 cdn和缓存,将静态资源上传到 cdn服务商,我们可以对资源开启长期缓存,因为每个资源的路径都是独一无二的,所以不会导致资源被覆盖,保证线上用户访问的稳定性。
3. 每次发布更新的时候,先将静态资源(js, css, img) 传到 cdn 服务上,然后再上传 html 文件,这样既保证了老用户能否正常访问,又能让新用户看到新的页面。

4.ExtractTextWebpackPlugin 分离 CSS

Webpack打包默认会把css和js打包在一块,然而我们通常习惯将css代码中放在标签中,而将js引用放在页面底部;将css代码放在页面头部可以避免 FOUC 问题(表现为页面内容已经加载出来,但是没有样式,过了一会儿样式文件加载出来之后页面回复正常);同时如果css和js分离也有利于这加强样式的可缓存性;这是我们需要ExtractTextWebpackPlugin来分离js与css使得样式文件单独打包。

在代码中想JavaScript模块一样引入css文件
import styles from ‘./style.css’
需要借助css-loader和 style-loader

module.exports = {
    module: {
        rules: [{
            test: /\.css$/,
            use: [ 'style-loader', 'css-loader' ]
        }]
    }
}
这样,CSS 会跟你的 JavaScript 打包在一起
通过加入ExtractTextWebpackPlugin,每个模块的css都会生成一个新文件,此时你可以作为一个单独标签添加到html文件中。
 plugins: [
        new ExtractTextPlugin( ({
            filename: '[name].css',//使用模块名命名
            allChunks: true
        })
    ]

5.UglifyJsPlugin代码压缩输出

代码压缩插件UglifyJsPlugin通过UglifyJS2来完成代码压缩,

6.html-webpack-plugin

自动生成html

new HtmlWebpackPlugin({
  filename: 'index.html', // 生成文件名
  template: path.join(process.cwd(), './index.html') // 模班文件
})

7.copy-webpack-plugin

拷贝资源插件

8.webpack-bundle-analyzer

编译模块分析插件

new BundleAnalyzerPlugin({
  analyzerMode: 'server',
  analyzerHost: '127.0.0.1',
  analyzerPort: 8889,
  reportFilename: 'report.html',
  defaultSizes: 'parsed',
  generateStatsFile: false,
  statsFilename: 'stats.json',
  statsOptions: null,
  logLevel: 'info'
}),
new CopyWebpackPlugin([
  {
    from: path.join(process.cwd(), './vendor/'),
    to: path.join(process.cwd(), './dist/'),
    ignore: ['*.json']
  }
])

编写一个插件

一个 webpack 插件由以下组成:

1.一个 JavaScript 命名函数。

2.在插件函数的 prototype 上定义一个 apply 方法。

3.指定一个绑定到 webpack 自身的事件钩子。

4.处理 webpack 内部实例的特定数据。

5.功能完成后调用 webpack 提供的回调。

Compiler 对象 (负责编译)

Compiler 对象包含了当前运行 Webpack 的配置,包括 entry、output、loaders 等配置,这个对象在启动 Webpack 时被实例化,而且是全局唯一的。Plugin 可以通过该对象获取到 Webpack 的配置信息进行处理。

compiler暴露的一些常用钩子
在这里插入图片描述

去除注释的插件

class RemoveCommentPlugin {
  constructor(options) {
    this.options = options
  }
  apply(compiler) {
    // 去除注释正则
    const reg = /("([^\\\"]*(\\.)?)*")|('([^\\\']*(\\.)?)*')|(\/{2,}.*?(\r|\n))|(\/\*(\n|.)*?\*\/)|(\/\*\*\*\*\*\*\/)/g
 
    compiler.hooks.emit.tap('RemoveComment', (compilation) => {
      // 遍历构建产物,.assets中包含构建产物的文件名
      Object.keys(compilation.assets).forEach((item) => {
        // .source()是获取构建产物的文本
        let content = compilation.assets[item].source()
        content = content.replace(reg, function (word) {
          // 去除注释后的文本
          return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? '' : word
        })
        // 更新构建产物对象
        compilation.assets[item] = {
          source: () => content,
          size: () => content.length,
        }
      })
    })
  }
}
 
module.exports = RemoveCommentPlugin

为什么选择 webpack

想要理解为什么要使用 webpack,我们先回顾下历史,在打包工具出现之前,我们是如何在 web 中使用 JavaScript 的。

在浏览器中运行 JavaScript 有两种方法。第一种方式,引用一些脚本来存放每个功能;此解决方案很难扩展,因为加载太多脚本会导致网络瓶颈。第二种方式,使用一个包含所有项目代码的大型 .js 文件,但是这会导致作用域、文件大小、可读性和可维护性方面的问题。
IIFE 使用方式产生出 **Make, Gulp, Grunt, Broccoli 或 Brunch 等工具。**这些工具称为任务执行器,它们将所有项目文件拼接在一起。
这些方式在优化打包结果方面变得更加困难,修改一个文件意味着必须重新构建整个文件。
由于nodejs的出现,和commonjs规范的出现,🙆我们可以不需要在浏览器里面增加script脚本,我们可以使用require导入模块。

是否可以有一种方式,不仅可以让我们编写模块,而且还支持任何模块格式(至少在我们到达 ESM 之前),并且可以同时处理资源和资产?

这就是 webpack 存在的原因。它是一个工具,可以打包你的 JavaScript 应用程序(支持 ESM 和 CommonJS),可以扩展为支持许多不同的资产,例如:images, fonts 和 stylesheets。
webpack 关心性能和加载时间;它始终在改进或添加新功能,例如:异步地加载 chunk 和预取,以便为你的项目和用户提供最佳体验。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值