最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Webpack打包优化

    正文概述 掘金(毛小星)   2021-02-17   391

    Webpack打包优化


    Webapck 4 之后默认为我们做了很多配置项,内部开启了很多优化功能。对于开发人员,这种开箱即用的体验显然是很好的,但是同时也会导致我们忽略了很多需要学习的东西,一旦出现什么问题的时候,我们就无从下手了,下面我们就来看一下主要的优化配置项。

    DefinePlugin

    DefinePlugin 是用来为我们的代码来注入全局成员的,在 production 模式下,这个插件就会默认开启。它会在我们的环境中注入了一个 process.env.NODE_ENV 这样一个环境变量,我们可以通过这个环境变量去判断运行环境,从而去执行一些相应的逻辑。

    const webpack = require('webpack')
    
    module.exports = {
      mode: 'none',
      entry: './src/main.js',
      output: {
        filename: 'bundle.js'
      },
      plugins: [
        new webpack.DefinePlugin({
          // 值要求的是一个代码片段
          API_BASE_URL: JSON.stringify('https://api.example.com')
        })
      ]
    }
    

    这样我们就可以直接在环境中使用 API_BASE_URL 这个变量了

    // main.js
    console.log(API_BASE_URL)
    

    Tree-shaking

    Tree-shaking 顾名思义就是摇树,伴随着摇这个动作,我们会将树上的枯树枝和枯树叶摇下来。而在我们的项目中 Tree-shaking 会将我们代码中没有引用的部分去掉,Tree-shaking 并不是某一个配置选项,它是一组功能搭配使用的效果。我们可以使用 optimization 去开启一些功能,optimization 就是优化的意思,下面我们来看看怎样去配置它

    module.exports = {
      mode: 'none',
      entry: './src/index.js',
      output: {
        filename: 'bundle.js'
      },
      optimization: {
        // 模块只导出被使用的成员
        usedExports: true,
        // 尽可能合并每一个模块到一个函数中
        concatenateModules: true,
        // 压缩输出结果
        // minimize: true
      }
    }
    

    我们可以将 usedExports 想象成它就是去标记"枯树叶"的,而 minimize 就是去摇下这些枯树叶的。而 concatenateModules 将所有的代码都尽可能的合并到一个函数中去,这样既提升了运行效率,又减少了代码的体积。这个特性又被称为 Scope Hoisting,这时 Webpack3 中提出的一个特性。

    • Tree-shaking 与 babel

    由于 Webpack 的发展比较快,所以我们在找资料的时候,找到的资料并不一定适用于我们当前的版本,Tree-shaking 更是如此,很多资料中都显示如果我们使用的 babel-loader 的话,就会导致 Tree-shaking 失效。因为 Tree-shaking 使用的前提就是必须使用 ES Modules 规范去组织我们的代码,而 @babel/preset-env 这个插件内部就会将 ES Modules 的代码转换为 commonjs 代码的方式,所以 Tree-shaking 就不能生效。但是实际你同时开启两者的话,Tree-shaking 还是会生效的,因为 @babel/preset-env 这个插件最新的版本内部将 ES Modules 转换为 commonjs 关掉了。

    module.exports = {
      mode: 'none',
      entry: './src/index.js',
      output: {
        filename: 'bundle.js'
      },
      module: {
        rules: [
          {
            test: /\.js$/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: [
                  // 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
                  // ['@babel/preset-env', { modules: 'commonjs' }]
                  // ['@babel/preset-env', { modules: false }]
                  // 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
                  ['@babel/preset-env', { modules: 'auto' }]
                ]
              }
            }
          }
        ]
      },
      optimization: {
        // 模块只导出被使用的成员
        usedExports: true,
        // 尽可能合并每一个模块到一个函数中
        // concatenateModules: true,
        // 压缩输出结果
        // minimize: true
      }
    }
    

    sideEffects

    Webpack4 中还新增了一个叫 sideEffects 的新特性,它允许我们去标识我们的代码是否有副作用,从而为 Tree shaking 提供更大的压缩空间。副作用就是模块去执行时除了导出成员之外所做的事情,sideEffects 一般只有我们在去开发一个 npm 模块的时候才会去使用,那是因为官网将 sideEffects 和 Tree shaking 混到了一起,所以很多人误认为它们两个是因果关系,其实它们两个的关系不大。 当我们去封装组件的时候,我们一般会将所有的组件都导入在一个文件中,然后通过这个文件集体导出,但是其他文件引入这个文件的时候,就会将这个导出文件的所有组件都引入

    // components/index.js
    export { default as Button } from './button'
    export { default as Heading } from './heading'
    
    // main.js
    import { Button } from './components'
    document.body.appendChild(Button())
    

    这样 Webpack 在打包的时候,也会将 Heading 组件打包到文件中,这时 sideEffects 就能解决这个问题

    module.exports = {
      mode: 'none',
      entry: './src/index.js',
      output: {
        filename: 'bundle.js'
      },
      optimization: {
        sideEffects: true,
      }
    }
    

    同时我们在 packag.json 中导入将没有副作用的文件关闭,这样就不会将无用的文件打包到项目中了

    {
      "name": "side-effects",
      "version": "0.1.0",
      "main": "index.js",
      "author": "maoxiaoxing",
      "license": "MIT",
      "scripts": {
        "build": "webpack"
      },
      "devDependencies": {
        "webpack": "^4.41.2",
        "webpack-cli": "^3.3.9"
      },
      "sideEffects": false
    }
    

    使用 sideEffects 的需要注意的是,我们的代码中真的没有副作用,如果有副作用的代码,我们就不能去这样配置了。

    // exten.js
    // 为 Number 的原型添加一个扩展方法
    Number.prototype.pad = function (size) {
      // 将数字转为字符串 => '8'
      let result = this + ''
      // 在数字前补指定个数的 0 => '008'
      while (result.length < size) {
        result = '0' + result
      }
      return result
    }
    

    例如我们在 extend.js 文件中为 Number 的原型添加一个方法,我们并没有向外导出成员,只是基于原型扩展了一个方法,我们在其他文件导入这个 extend.js

    // main.js
    // 副作用模块
    import './extend'
    console.log((8).pad(3))
    

    如果我们还标识项目中所有模块没有副作用的话,这个添加在原型的方法就不会被打包进去,在运行中肯定会报错,还有就是我们在代码中导入的 css 模块,也都是副作用模块,我们就可以在 package.json 中去标识我们的副作用模块

    {
      "name": "side-effects",
      "version": "0.1.0",
      "main": "index.js",
      "author": "maoxiaoxing",
      "license": "MIT",
      "scripts": {
        "build": "webpack"
      },
      "devDependencies": {
        "webpack": "^4.41.2",
        "webpack-cli": "^3.3.9"
      },
      "sideEffects": [
        "./src/extend.js",
        "*.css"
      ]
    }
    

    这样标识的有副作用的模块也会被打包进来。

    Webpack 代码分割(Code Splitting)

    模块化的优势固然很明显,但是也存在一些弊端,就是在我们的项目中所有的代码都会被打包到一起,如果我们的项目过大的话,那么我们的打包结果就会特别大。但是实际的情况是,我们在首次加载的时候,并不是所有的模块都是必须加载的,但是这些模块又被打包到一起,所以一方面在浏览器运行的时候会慢,一方面也会浪费一些流量和带宽。所以合理的方式就是将我们的代码按照一定的规则打包到多个 js 文件中去,做分包处理、按需加载,这样我们就会大大提高我们的应用的响应速率。那么有人可能会想到 Webpack 不就是将我们代码中散落的代码合并到一个函数中去执行,从而去提高效率,这里为什么又要做分包处理,不是自相矛盾吗?其实任何事情都是物极必反,Webpack 做代码合并是因为我们在开发中往往模块化颗粒度太细,所以 Webpack 必须将很多代码合并到一起,但是如果总体代码量过大的话,就会导致我们的单个打包文件过大,反而影响效率。所以模块化颗粒度太小不行,太大也不行,而 Code Splitting 就是为了解决我们模块化颗粒度太大的问题。

    • 多入口打包

    多入口打包就是将一个页面作为一个打包入口,而对于不同页面中公共的部分再去提取到公共的文件中去,而多入口打包的配置也很容易

    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    
    module.exports = {
      mode: 'none',
      entry: { // 多入口打包,多个入口文件
        index: './src/index.js',
        album: './src/album.js'
      },
      output: {
        filename: '[name].bundle.js' // 由于多入口打包,采用占位符
      },
      optimization: {
        splitChunks: {
          // 自动提取所有公共模块到单独 bundle
          chunks: 'all'
        }
      },
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              'style-loader',
              'css-loader'
            ]
          }
        ]
      },
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          title: 'Multi Entry',
          template: './src/index.html',
          filename: 'index.html',
          chunks: ['index'] // 配置chunk,防止同时载入
        }),
        new HtmlWebpackPlugin({
          title: 'Multi Entry',
          template: './src/album.html',
          filename: 'album.html',
          chunks: ['album']
        })
      ]
    }
    
    • Webpack 按需加载

    按需加载是我们在开发中常见的需求,我们在处理打包的时候,我们可以需要哪个模块是,再加载哪个模块。Webpack 中支持动态导入的方式去支持按需加载我们的模块,所有动态加载的模块都会被自动分包,相比于分包加载的方式,动态加载的方式更加灵活。 例如我们有两个模块 album 和 posts,我们就可以使用 import 去实现动态导入,import 返回一个 promise 对象,

    // ./posts/posts.js
    export default () => {
        const posts = document.createElement('div')
        posts.className = 'posts'
        ...
        return posts
    }
    
    // ./album/album.js
    export default () => {
        const album = document.createElement('div')
        album.className = 'album'
        ...
        return album
    }
    
    // import posts from './posts/posts'
    // import album from './album/album'
    
    const render = () => {
      const hash = window.location.hash || '#posts'
    
      const mainElement = document.querySelector('.main')
    
      mainElement.innerHTML = ''
    
      if (hash === '#posts') {
        // mainElement.appendChild(posts())\
        // 魔法注释:给模块重命名
        import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
          mainElement.appendChild(posts())
        })
      } else if (hash === '#album') {
        // mainElement.appendChild(album())
        import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
          mainElement.appendChild(album())
        })
      }
    }
    
    render()
    
    window.addEventListener('hashchange', render)
    

    css 的模块化打包

    • MiniCssExtractPlugin 是一个能够将 css 文件从打包文件中单独提取出来的插件,通过这个插件我们就可以实现 css 模块的按需加载。
    • optimize-css-assets-webpack-plugin 是一个能够压缩 css 文件的插件,因为使用了 MiniCssExtractPlugin 之后,就不需要使用 style 标签的形式去加载 css 了,所以我们就不需要 style-loader 了
    • terser-webpack-plugin 因为 optimize-css-assets-webpack-plugin 是需要使用在 optimization 的 minimizer 中的,而开启了 optimization,Webpack 就会认为我们的压缩代码需要自己配置,所以 js 文件就不会压缩了,所以我们需要安装 terser-webpack-plugin 再去压缩 js 代码
    // 安装 mini-css-extract-plugin
    yarn add mini-css-extract-plugin --dev
    // 安装 optimize-css-assets-webpack-plugin
    yarn add optimize-css-assets-webpack-plugin --dev
    // 安装 terser-webpack-plugin
    yarn add terser-webpack-plugin --dev
    

    接下来我们就可以配置它们了

    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const MiniCssExtractPlugin = require('mini-css-extract-plugin')
    const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
    const TerserWebpackPlugin = require('terser-webpack-plugin')
    
    module.exports = {
      mode: 'none',
      entry: {
        main: './src/index.js'
      },
      output: {
        filename: '[name].bundle.js'
      },
      optimization: {
        minimizer: [
          new TerserWebpackPlugin(), // 压缩 js 代码
          new OptimizeCssAssetsWebpackPlugin() // 压缩模块化的 css 代码
        ]
      },
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              // 'style-loader', // 将样式通过 style 标签注入
              MiniCssExtractPlugin.loader, // 使用 MiniCssExtractPlugin 的 loader 就不需要 style-loader 了
              'css-loader'
            ]
          }
        ]
      },
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          title: 'Dynamic import',
          template: './src/index.html',
          filename: 'index.html'
        }),
        new MiniCssExtractPlugin()
      ]
    }
    

    输出 hash 文件名

    一般我们去部署前端的资源文件的时候,我们都会启用服务器的静态资源缓存,这样对于用户的浏览器而言就可以缓存住我们的静态资源,后续就不再需要请求服务器去请求这些静态资源了,这样我们的应用的相应速度就会有一个大幅度的提升。不过开启客户端的静态资源缓存也会有问题,如果我们在设置缓存时间过短的话,那么缓存就没什么意义了,而设置过长的话,一旦应用发生了更新,就没有办法即时更新到客户端。为了解决这个问题,我们就需要在生产模式下,为文件名使用 hash,这样一旦我们的文件资源发生改变,我们的文件名称也会随之发生改变,而对于客户端而言,全新的文件名也就意味着全新的请求,这样我们就可以将缓存时间设置的非常长,也不用去担心文件不更新的问题。

    • hash

    项目级别的 hash,一旦任何文件发生修改,都会生成新的 hash

    • chunkhash

    只要是同一路的打包,hash 都是相同的,例如一个模块内的 js 和 css 的 hash 前缀都是相同的

    • contenthash

    文件级别的 hash,根据文件内容输出的 hash 值,只要是不同的文件就有不同的 hash 值,这也是最推荐的 hash 方式

    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const MiniCssExtractPlugin = require('mini-css-extract-plugin')
    const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
    const TerserWebpackPlugin = require('terser-webpack-plugin')
    
    module.exports = {
      mode: 'none',
      entry: {
        main: './src/index.js'
      },
      output: {
        filename: '[name]-[contenthash:8].bundle.js'
      },
      optimization: {
          ...
      },
      module: {
          ...
      },
      plugins: [
        ...
        new MiniCssExtractPlugin({
          filename: '[name]-[contenthash:8].bundle.css'
        })
      ]
    }
    
    

    起源地下载网 » Webpack打包优化

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元