webpack从入门及实战(三)webpack高级概念

Tree Shaking

tree shaking是一个术语,翻译过来是摇树的意思。可以将它理解为整个app是一棵树。上面有实际用到的源码和 library,是树上活的绿叶。也有一些无用的代码,是树上的枯叶。为了让这棵树更茁壮的成长,你必须摇动这棵树,让枯叶落下。

首先改一下sayHi.js

// sayHi.js
export function sayHi(){
  console.log('hi')
}
export function sayHello(){
  console.log('hello')
}

index中引入一下

// index.js
import { sayHi } from "./sayHi";

sayHi()

这时候我们把代码打包,注意我们并没有用到sayHi.js中的sayHello方法,但是打包之后却发现sayHello也被打包进去了
在这里插入图片描述
而这个sayHello方法,就像一片枯叶,而我们理想的情况,肯定是没有引入的文件就不再打包进去,这时候就需要配置tree shaking了

配置tree shaking,首先需要配置package.json中的一个 “sideEffects”
这个是用来将文件标记哪些文件没有导出任何东西,但是在实际项目中是有用的,比如一些向window添加方法的类库,或者是css这类文件

"sideEffects": [
    "./src/some-side-effectful-file.js"
  ]

如果没有这类文件,那么可以直接将sideEffects设置为false

然后我们需要将mode切换为production,开发模式下默认是没有打开tree shaking的,将模式切换为production之后,我们再次进行打包
打包之后的代码是压缩过的,但我们可以通过检索console.log来确定tree shaking是否生效
在这里插入图片描述
可以看到console.log只有一个,是console.log(‘hi’),证明tree shaking已经生效,sayHello并没有被打包
好了,我们通过tree shaking成功将代码压缩了几个字节…虽然在这里看起来意义不大,但是在具有复杂的依赖树的大型应用程序上运行时,tree shaking 或许会对 bundle 产生显著的体积优化。

开发模式和生产模式的区分打包

开发环境(development)和生产环境(production)的构建目标是不同的。开发环境中,我们需要的是一个实时重载的devServer和完整的source map。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。
通常我们会使用一个webpack-merge工具来进行配置
首先安装
npm install --save-dev webpack-merge
然后我们新建一个单独的config文件夹,用于存放webpack的配置文件,然后新建3个配置文件
在这里插入图片描述
在common中,我们存放一些通用的配置,比如entry,output,loader,plugins这些配置
然后在dev和prod中分别写开发和生产环境不同的配置

// dev
const {merge} = require("webpack-merge");
// 引入通用配置
const common = require("./webpack.common.js");

module.exports = merge(common, {
  // 设置开发环境独有的配置
  mode: "development",
  devtool: "cheap-module-eval-source-map",
  devServer: {
    // 告诉devServer在哪里查找文件
    contentBase: "../myDist",
    // 启动后自动浏览器打开
    open: true,
    // 端口
    port: 4396,
    hot: true,
  },
});
// prod
const {merge} = require("webpack-merge");
// 引入通用配置
const common = require("./webpack.common.js");

module.exports = merge(common, {
  // 设置生产环境独有的配置
  mode: "production",
  devtool: "false",
});

这里由于我把所有配置文件都放到了一个新的文件夹中,所以路径要做一些修改,如果还是放在根目录下的话则不需要做修改

配置文件完成后我们将package.json中的npm scripts做一些修改

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config ./config/webpack.prod.js",
    "start": "webpack-dev-server --config ./config/webpack.dev.js"
  },

这样,我们就可以通过npm run start 运行一个开发环境的dev server,然后通过npm run build来打包一套生产环境的代码了

代码分离 Code Splitting

试想如下情况

// 引入一个第三方库
import _ from 'lodash'

console.log(_.join(['1','2','3']))
// 此处略去1万行业务代码....

这时候进行打包,第三方库的代码会和我们大量的业务代码被整合至一个文件内,但是第三方库的代码通常是不会发生任何改变的,而我们的业务逻辑则会经常发生变化,如果每次打包都打包到一个文件内,那么每一次用户都需要重新加载所有的代码,这对于性能的优化很明显是不利的,正确的做法应该是将第三方模块抽取出来,分离成多个文件,单独加载,这样业务代码发生变化只会重新加载单独的业务代码部分,而不变的第三方模块则不必重新加载

我们可以通过以下的方法简单的实现分割
首先去掉在index.js文件中的lodash引入
然后新建一个lodash.js文件

// lodash.js
import _ from 'lodash'
window._ = _

在webpack配置中增加一个entry

entry:{
	main:'index.js的路径',
	lodash:'lodash.js的路径'
}

配置好之后再进行打包,可以看到生成了2个文件,分别是main.js和lodash.js
这样业务代码如果后续发生变更,只有main.js会发生改变,lodash.js不会发生任何改变,而它在用户的浏览器里是有缓存的,所以用户只需加载一个新的main.js就可以了,可以有效的提升后续页面的打开速度

但是这样的手动添加entry肯定是不方便的,webpack很有竞争力的一个功能就是自动的代码分割,我们可以使用官方的插件来进行代码分割

需要在config文件中添加一个optimization项,optimization里通常都是对打包的一些优化项,这里代码分割配置的是splitChunks项

optimization: {
    splitChunks: {
      // chunks代表对同步或者异步引入的代码块进行分割,默认是async,通常会设置为all,还有initial代表只对同步引入代码进行分割
      chunks: "async",
      // 引入的包体积要大于minSize才会被分割,单位是字节
      minSize: 30000,
      // 引入包的体积如果大于maxSize,webpack会尝试再次对这个包进行分割,maxSize通常配置的比较少,因为第三方的包通常无法再次进行分割
      maxSize:0,
      // minChunks指引入的包至少被用了多少次才进行代码分割
      minChunks: 1,
      // 最大引入的模块数,大于这个模块数的模块不做分割,通常不做修改
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      // 文件生成时默认的文件名连接符
      automaticNameDelimiter: "~",
      name: true,
      // 如果要分割同步引入的包,并且这个包符合上面所有的要求,那么会进入cacheGroups根据里面的规则进行分割
      cacheGroups: {
        vendors: {
          // 规则
          test: /[\\/]node_modules[\\/]/,
          // 优先级
          priority: -10,
        },
        default: {
          minChunks: 2,
          priority: -20,
          // 要被分割的代码块如果之前已经被打包过,就不再重新打包,直接复用
          reuseExistingChunk: true,
        },
      },
    },
  },

更多的一些配置可以查看官方的文档,不过只有英文版,看起来可能有些费时

懒加载

有的时候我们希望一些用到的类库或者组件不要在页面一打开的时候全部加载,而是在用到的时候再加载,从而优化网站首次打开的速度,这时候就要使用懒加载了
要实现懒加载其实很简单,使用异步方式来引入用到的库或者组件就可以了

// sayHi.js
export function sayHi(){
  console.log('hi')
  return 'hi'
}
// index.js
document.addEventListener("click", () => {
  // 异步引入组件的写法,返回的是一个promise对象
  import("./sayHi.js").then((res) => {
    const el = document.createElement("div");
    el.innerText = res.sayHi();
    document.body.append(el)
  });
});

也可以使用异步函数的方式引入来使代码更直观

// index.js
document.addEventListener("click", async () => {
  // 异步函数的方式
  const res = await import("./sayHi.js")
  const el = document.createElement("div");
  el.innerText = res.sayHi();
  document.body.append(el)
});

这2种方法都可以实现异步加载sayHi.js的效果
可以看到页面首次打开时候加载了3个js文件
在这里插入图片描述
当我们点击页面,触发事件之后,才会多引入一个js文件,然后内部的逻辑被执行
在这里插入图片描述
通过异步加载的语法,我们可以非常便捷的实现懒加载,在日常的项目中,我们通常会在项目的路由配置中使用这种语法来实现懒加载
不过要注意一点,因为无论是promise对象还是异步函数都是低版本浏览器不支持的,如果想在低版本浏览器中使用需要使用polyfill来使浏览器支持这些新的语法

Preloading,Prefetching

试想如下情况

// index.js
document.addEventListener("click", () => {
  // 业务逻辑
  const el = document.createElement("div");
  el.innerText = 'hi';
  document.body.append(el);
});

如果里面的业务逻辑非常复杂的话,那么加载的耗时也会更长
但是,这里面的业务逻辑却在页面刚加载出来是没有用的,一直要到页面被点击,才会起作用,那么我们就可以用懒加载的方式将里面的业务逻辑引入

// sayHi.js
function sayHi (){
  const el = document.createElement("div");
  el.innerText = 'hi';
  document.body.append(el);
}
export default sayHi
// index.js
document.addEventListener("click", async () => {
  const res = await import("./sayHi.js");
  res.default()
});

这样就可以让页面在被点击时候才引入sayHi.js里面的内容
但是于此同时也有一个问题,如果我们的业务逻辑很复杂,假设有1m的代码,那么用户在点击之后,就需要等待加载1m的文件后,才会执行下面的逻辑,这很明显是会影响用户体验的,所以我们可以通过Prefetching来让页面在带宽空闲时,自动加载这部分的业务逻辑,从而用户在点击的时候,这部分逻辑已经加载完毕,但同时又不影响首屏的性能
Preloading和Prefetching类似,但是Preloading会在页面开始加载时候直接加载,而不会等到页面有空闲带宽时候才加载,所以我们通常会使用Prefetching

实现Prefetching很简单,只需要在引入的时候加上一条/* webpackPrefetch: true */即可

// index.js
document.addEventListener("click", async () => {
  const res = await import(/* webpackPrefetch: true */ "./sayHi.js");
  res.default()
});

在之前我们的页面在一开始只会加载5个文件
在这里插入图片描述
点击后才会额外加载一个js文件
在这里插入图片描述

添加了prefetch之后,我们的页面会在一开始主页面所有逻辑加载完毕后,自动加载多一个js文件
在这里插入图片描述
而在点击后,则会使用之前prefetch的文件
在这里插入图片描述
这种方法也正是webpack比较推荐的一种做法,所以在splitChunks的配置中,默认只对async方式的代码做分割,因为只有异步方式加载的代码才能真正起到首屏性能的优化,同步代码更多只是后续打开的时候利用缓存做的优化

对样式代码进行分割

在之前的打包中,webpack在做样式的打包的时候,会直接打包到js里面
如果想在打包生成代码的时候,引入了样式文件的话,就把样式文件直接打包到目录下。而不是直接引入到js文件里面。那么就需要引入一个插件 mini-css-extract-plugin
安装之后,进行一些简单的配置即可使用

 module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
      {
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              importLoaders: 1,
              modules: true,
            },
          },
          "less-loader",
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "/css/[name].css",
    }),
  ],

将style-loader替换成该插件就可以了,插件的具体配置项可以参考这里
不过要注意这个插件不支持热重载,所以通常会在prod环境下使用,正常的开发环境还是使用style-loader使项目可以热重载比较好一些

浏览器缓存

在之前的打包中,我们生成的文件名都是固定的,这可能导致我们在更新了代码重新上传之后,浏览器误认为我们没有更新,直接沿用缓存的代码,所以通常我们会给打包生成的代码添加上哈希值,来让浏览器作为区分,从而使我们更新的代码会被重新读取

output: {
    // filename: "index.js",
    // 这样可能会在更新代码后浏览器继续使用旧的缓存
    filename: "js/[name].[hash].js",
    chunkFilename: "js/[name].[chunkhash].js",
    path: path.resolve(__dirname, "../myDist"),
  },

给打包的文件名添加上一个哈希值就可以了

Shimming

尽管webpack推荐的做法是使用模块,但是在使用一些比较老的第三方库的时候,比如jQuery中的$,这个时候就需要在项目中添加一个全局变量,但是这很明显不符合模块的导入规则,所以webpack提供了一个shimming来处理这种情况
以jQuery为例

// index.js
import $ from "jquery";
$(document).ready(function () {
  $("body").css("background", "green");
});

这样使用是没有任何问题的
但是如果想用全局变量,不使用import这种方式的话,那就需要去做一下配置,使用webpack提供的一个插件

// webpack.config
plugins:[
	new webpack.ProvidePlugin({
      $: "jquery",
    })
]
// index.js
// import $ from "jquery";
// 不引入jquery
$(document).ready(function () {
  $("body").css("background", "green");
});

在配置插件后,即使不引入jquery,也可以正常运行
不过要注意的是这种方法虽然可以注入全局变量,但是官方实际依旧是不推荐的,所以除非使用一些比较老的第三方库,否则最好不要使用这种方法
在这里插入图片描述

关于webpack的概念介绍的也差不多了,下一篇会介绍实战中的一些配置以及性能的优化

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值