hmr webpack 不编译_webpack性能优化以及webpack5展望(面试相关)

天下没有白吃的午餐。

96ae56c70333c14c1c7d83758567a302.png

前言

有一位波斯国的国王,他勤政爱民,善良聪明,全国的人民在他的领导下,过着衣食无忧、安居乐业的生活。

可是随着国王一天天老去,他开始为一件事担心,就是怕自己死了之后,人民不能再像现在一样过着幸福的生活,怎么办呢?一天,他把全国的有识之士都召集了起来,让他们找寻出一个能确保人民生活幸福的永世法则。 

 3个月后,学者们呈给国王3本6寸厚的帛书,并且说:“这3本书囊括了天下的知识,只要人民能够把它读完,就能世代无忧无虑地生活下去了”。

国王看了看那3本厚厚的书,很不以为然,因为他知道,人民是不会花那么多的时间看完这些书的。他让这些学者继续钻研。

2个月后,学者们呈给国王一张纸,国王在看了这张纸之后,非常满意,说:“很好,有这几个字我死也瞑目了,只要我的子民能按照纸条上所写的那样去做,相信他们一定能过上幸福安康的生活。”

请问纸上写的是什么?

以上内容是一个简单的鸡汤故事,与本篇内容无关,却蕴含人生哲理。

今天的主题是关于webpack的性能优化,以及即将发版的webpack5,有什么新的更新

webpack性能优化

之前在拉钩教育买过一节课《webpack原理与实践》。此课程中以独到的见解来阐述webpack的工作原理和运行机制,并传递一种思想就是「 带着问题去看源码 」,明确“查阅”而不是“死扣”。

文中提到一个生产环境下的优化插件DefinePlugin,DefinePlugin 是用来为我们代码中注入全局成员的。

// ./webpack.config.js
const webpack = require('webpack')
module.exports = {
  // ... 其他配置
  plugins: [
    new webpack.DefinePlugin({
      // 值要求的是一个代码片段
      API_BASE_URL: JSON.stringify('https://api.example.com')
    })
  ]
}

文中会讲解一些小技巧,以及一些插件常见的坑。

比较有意思的是我提了一个问题:「 程序是如何识别rc类型文件的,webpack是如何做到.webpackrc.js来替换webpack.config.js的?」

老师的回复是一个库:「 cosmiconfig 」这个库会自动搜索配置文件。

除了拉钩教育还在开课吧买过一个小课程《Web前端架构师高级进阶必备技能》,此课程中讲的东西比较多,有一节课是「 webpack原理剖析 」,是从源码层面讲解webpack如何打包编译,以及手动实现一个简单的webpack。

除了以上,还去B站,找到了一份条理比较清晰的《weebpack实战课程》。此课程是出自尚硅谷,从使用层面出发,讲解webpack能解决什么问题,在项目中如何优化,如何配置。

本文以下的大部分内容都是以上课程的笔记,如果说的不明白,可以自行去查阅实战,也可留言探讨。

关于webpack的性能优化,从两方面入手:开发环境和生产环境。

开发环境性能优化

在开发环境下,程序员会着重关注两点,一个是实时编译,另一个调试方便。

提到热更新就会想到HMR功能,开启webpack配置中的devServer的hot属性即可开启HMR功能,HMR会极大的提高构建速度,并且一个模块发生变化,只会重新打包一个模块,而不是打包所有模块。

相对于样式文件,style-loader已经内部实现了对HMR的支持。

相对于HTML文件,改变不会自动更新(默认不支持HMR),需要我们做一些配置。(引文html很少变化,一般情况我们不作处理)

// 修改entry入口,将html文件引入
module.exports = {
   entry: ["./src/index.js", "./src/index.html"],

   // ... 其他配置
}

相对于JS文件,每次修改会全局刷新,我们想要的效果是每次修改那个模块,那个模块更新,不需要浏览器刷新。如下例子

//a.js
 function add(x,y){
    console.log("add加载了")
    return x+y;
 }
 export default add
//index.js
import add from "./a.js"

console.log("index.js被加载了")
if(module.hot){
    module.hot.accept("./a.js",function(){
        //监听a.js的变化,一旦发生变化,其他模块不会重新打包构建
        add()
    })
}

通过module.hot来判断是否开启HMR功能,通过accept监听某个模块是否变化。

source-map 是一种提供源代码构建后代码映射的技术,如果构建后的代码出现问题,可以通过映射追踪源代码的错误位置。

source-map分两种类型:内联和外部。内联就是直接会在打包的js内部生成,而且内联的速度比较快。而外部就是在外部单独生成一个souce-map文件。

//[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
//source-map:外部
//错误代码的准确位置

//inline-source-map:内联  只生成一个内联的source-map
//错误代码的准确位置

//hidden-source-map:外部  在外部生成一个source-map文件
//错误代码的错误原因,不能追踪到源代码的错误位置,只能提示构建后的代码错误位置。

//eval-source-map:内联  每一个文件对应的source-map都在eval里
//错误代码的准确位置

//nosource-source-map:外部
//错误代码的准确位置,没有任何源代码信息

//cheap-source-map
//错误代码的准确位置,只精确到行

//cheap-module-source-map
//module会将loader的source-map加入

//开发环境:速度快,调试友好
    //速度 eval>inline>cheap>...
     //eval-cheap-source-map会变得更快

    //调试更友好的是
    //source-map>cheap-module-source-map>cheap-source-map

    //折中化方案(脚手架默认配置方案)
    //eval-source-map

//生产环境:源代码要不要隐藏?
    //内联会让体积非常大,所以一般不考虑内联
    //如果需要隐藏源代码可以考虑
    //nosource-source-map  全部隐藏
    //hidden-source-map   只隐藏源代码

生产环境性能优化

正常情况下,一个文件只能被一个loader处理。当一个文件要被多个loader处理,那么一定要指定loader执行的先后顺序。

oneOf是让一个loader处理一类文件,会让loader的性能更好。需要注意的是不能有两个配置处理同一种文件,比如eslint-loader,所以我们要把eslint-loader放外面。

  module: {
    rules: [
      {
        test: /\.js$/,
        exclude:/node_modules/,
        enforce:"pre",
        loader:"eslint-loader",
        options:{
          fix:true,
        }
      },
      {
        oneOf: [
          {
            test: /\.css$/,
            use: ["style-loader", "css-loader"],
          },
          {
            test: /\.(png|jpg|gif|svg)$/,
            use: ["file-loader"],
          },
          //...
        ]
      }
    ]
  }

除了oneOf这种优化构建速度的方案,我们还可以从缓存入手。babel-loader提供了一个cacheDirectory的属性可以帮我们实现babel缓存,他会在第二次构建的时候,读取之前的缓存。

  module: {
    rules: [
      {
        test: /\.js$/,
        exclude:/node_modules/,
        loader:"babel-loader",
        options:{
          cacheDirection:true,
        }
      }
      //...
    ]
  }

除了babel缓存还有一种缓存是文件资源缓存,通过hash来实现。

在webpack中3种hash,每次webpack构建会生成一个唯一的hash值。

//因为css和js同时使用一个hash值
//如果重新打包,会导致所有缓存失效。
output: {
  filename: "bundle.[hash:10].js",
  path: path.resolve(__dirname, "dist"),
},
plugins: [
  new MiniCssExtractPlugin({
    filename:"css/main.[hash:10].css"
  }),
  new HtmlWebpackPlugin({
    template: "./index.html",
  }),
],

chunkhash是根据chunk生成的hash值,如果打包来源于同一个chunk,那么hash值就一样。

还有一种hash是contenthash,它是根据文件内容生成hash值。不同文件hash值一定不一样。

所以我们常用contenthash来做文件资源缓存,它可以让代码在线上运行的时候,更好的利用缓存。

接下来我们聊一下 tree-shaking,tree-shaking是用来去除无用的代码。它的前提是你必须使用ES6模块化,环境为生产环境。

举个例子,比如untils.js里有很多导出的方法,但是我们只用了其中的一个。那么untils.js中的其他没有用的代码将不会被webpack打包编译。

//untils.js
export const add=()=>{
  //...
}
export const muilt=()=>{
  //...
}

//index.js
import { add } from "../../untils"
//tree-shaking 就是通过import将没用使用过的代码去除掉,比如muilt方法

但是如果代码中引入css模块呢?css模块也会被tree-shaking处理掉吗?

这里需要我们去package.json中设置:
//...
//"sideEffects":false,//所有代码都没有副作用(都可以进行tree-shaking)
"sideEffects":["*.css"],//css等文件不会进行tree-shaking。

如果我们生产环境我们需要第三方依赖,我们想要单独打包,我们如何做呢?

这里会用到optimization:

//...
optimization:{
  splitChunks:{
     chunk:"all"
  }
}

他会自动分析多入口chunk中,有没有公共的文件,如果有会打包成单独的一个chunk。

还有一种方法就是在import动态导入的时候添加,在路由中比较常见。

//...
component:()=>import(/*webpackChunkName: "text"*/ "./../views/text.vue")

import动态导入语法,能将此文件单独打包。

如果我们想按需加载,也就是当事件触发的时候,才加载所需文件,怎么设置?

这是典型的懒加载,在使用的时候加载。除了懒加载还有预加载,看起来效果和懒加载一样,但是其实已经偷偷加载了,当你点击的时候会从内存中获取。

//如果把import放在头部是正常加载,也是并行加载。
//把import语法放在异步的回掉中处理。
document.getElementById("btn").onclick=function(){
   //懒加载
   import(/*webpackChunkName:'test'*/"./test").then(
     (res)=>{
     //....
     }
   )
   //预加载:会提前加载(空闲的时候偷偷加载,不是并行加载)
   //等其他资源加载完毕的时候,偷偷加载,兼容行不是很好
   import(/*webpackPreFetch:true */"./test").then(
     (res)=>{
     //....
     }
   )
}
如果项目比较大,js文件比较多,还有一种优化打包速度的方案就是 多进程打包,它是通过thread-loader来实现的。比如babel-loader需要把编译转换大量js。
{
  test: /\.js$/,
  exclude: /node_modules/,
  use: [
    {
      //多进程打包
      loader: "thread-loader",
      options: {
        workers: 2,
      },
    },
    {
      loader: "babel-loader",
      options: {
        cacheDirection:true,
      }
    }
  ]
}

在一些项目中,我们可能会通过CDN引入一些第三方的库或资源,我们不希望webpack打包编译的时候打包进来,怎么设置?

这个时候就会用到exterals,它的主要作用就是在运行编译的时候排除外部依赖。官网给了一个很贴切的例子:

//index.html
"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(/* ... */);

exterals是完全不需要打包,但是如果我们不想用CDN,就想要本地打包,并且不想重复打包,怎么办?

这里就用到dll技术,这里需要分几步,第一步:对node_modules里的第三方库单独打包,首先我们创建一个webpack.dll.js(名字可以随意,这里用dll)文件。

//webpack.dll.js
const path = require("path");
const webpack = require("webpack");

module.exports = {
  mode: "production",
  entry: {
    //打包输出的名字为键,值中的jquery为要打包的库
    jquery: ["jquery"],
  },
  output: {
    path: path.resolve(__dirname, "dll"),
    filename: "[name].js",
    //打包的库里面向外暴露出去的内容叫什么
    library: "[name]_[hash]",
  },
  plugins: [
    new webpack.DllPlugin({
      //映射库暴露的内容名字
      name: "[name]_[hash]",
      path: path.resolve(__dirname, "dll/manifest.json"),
    }),
  ],
};

通过webpack --config webpack.dll.js命令打包之后,会生成dll文件目录,其下有两个文件,一个是jquery,一个是manifest.json映射文件。

第二步就是告诉webpack,哪些库不需要打包,并把之前dll打包的库映射到项目中(使用时名字得变)。

第三步,借助add-asset-html-webpack-plugin插件引入到html中。

//webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpack = require("webpack");
const AddAssetHtmlWebpackPlugin = require("add-asset-html-webpack-plugin");

module.exports = {
  mode: "production",
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: "Hello",
      template: "src/index.html",
      // minify: {
      //   //移除空格
      //   collapseWhitespace: true,
      //   //移除注释
      //   removeComments: true,
      // },
      // filename: "html/admin.html",
    }),
    //告诉webpack哪些库不需要打包,同时使用时名称也的变。
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, "dll/manifest.json"),
    }),
    //将某个文件打包输出去,并在html中自动引入该资源
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, "dll/jquery.js"),
    }),
  ],
}

输出的Html:



<body>
  <div id="title">Hello Worlddiv>
  <p>say you hello-p>
  <script src="jquery.js">script>
  <script src="bundle.js">script>
body>

性能优化总结

以上性能优化,从两方面入手:开发环境和生产环境。

主要优化的点是代码构建速度,代码调试性能,代码运行效率等。

53ad70ca4ed0aaca31a430f38ce945b8.png

webpack5展望

webpack5还没有正式发布,但是提供了实验版。github关于webpack5的说明文档地址为:https://github.com/webpack/changelog-v5

文章底部原文链接即为github地址,可以点进去查看webpack5的详细说明文档。

我们也可以下载webpack5实验版,尝试一下新的更新。

npm i webpack@next webpack-cli -D
此版本重点关注以下内容:
  • 通过持久化缓存提高构建性能。

  • 通过更好的算法和默认值来改善长期缓存。

  • 通过更好的tree-shaking(webpack4遗留问题)和代码生成来改善捆绑包的大小。

  • 清除处于怪异状态的内部结构,同时在V4中引入功能而不引入破坏性的更改。

  • 我们试图通过引入突破性的变化来为将来的特性做准备,允许我们尽可能长时间地使用v5。

webpack5会自动停止填充一些核心模块,比如自动删除Node.js Polyfills,而专注于与前端兼容的模块。

添加一些长期缓存的hash算法,在生产环境是默认启用的。

不再以id(0,1,2)命名,优化内部chunk命名规则。

tree-shaking功能更加强大,能够处理多个模块、嵌套模块之前的关系。也能处理对CommonJS的tree-shaking。

优化output,可以生成ES5和ES6/ES2015的代码。

优化代码分割SplitChunk,可以更精确的实现js,以及css的代码分割。

更多的内容可以移步github(点击下方原文链接查看)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值