webpack4心得編譯篇&體積篇

背景

衆所周知,webpack做爲主流的前端項目利器,從編譯到打包提供了不少方便的功能。本文主要從編譯和體積兩個篇章闡述筆者總結的實踐心得,但願對你們有幫助。css

編譯篇

vendor文件單獨打包

vendor文件即依賴庫文件,通常在項目中不多改動。單獨打包能夠在後續的項目迭代過程當中,保證vendor文件可從客戶端緩存讀取,提高客戶端的訪問體驗。html

解決方案:經過在vendor.config.js文件中定義,在webpack.config.{evn}.js中引用來使用。 vendor.config.js示例前端

module.exports = {
  entry: {
    vendor: ['react', 'react-dom', 'react-redux', 'react-router-dom', 'react-loadable', 'axios'],
  }
};

複製代碼

vendor文件預打包

vendor單獨打包以後,仍是有一個問題。編譯的過程當中,每次都須要對vendor文件進行打包,其實這一塊要是能夠提早打包好,那後續編譯的時候,就能夠節約這部分的時間了。vue

解決方案:定義webpack.dll.config.js,使用 DLLPlugin 提早執行打包,而後在webpack.config.{evn}.js經過 DLLReferencePlugin 引入打包好的文件,最後使用AddAssetHtmlPlugin往html裏注入vendor文件路徑 webpack.dll.config.js示例node

const TerserPlugin = require('terser-webpack-plugin');
const CleanWebpackPlugin = require("clean-webpack-plugin");
const webpack = require('webpack');
const path = require('path');
const dllDist = path.join(__dirname, 'dist');

module.exports = {
  entry: {
    vendor: ['react', 'react-dom', 'react-redux', 'react-router-dom', 'react-loadable', 'axios', 'moment'],
  },

  output: {
    path: const dllDist = path.join(__dirname, 'dist'),
    filename: '[name]-[hash].js',
    library: '[name]',
  },
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          parse: {
            ecma: 8,
          },
          compress: {
            ecma: 5,
            warnings: false,
            comparisons: false,
            inline: 2,
          },
          mangle: {
            safari10: true,
          },
          output: {
            ecma: 5,
            comments: false,
            ascii_only: true,
          },
        },
        parallel: true,
        cache: true,
        sourceMap: false,
      }),
    ],
  },
  plugins: [
    new CleanWebpackPlugin(["*.js"], { // 清除以前的dll文件
      root: dllDist,
    }),
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
    new webpack.DllPlugin({
      path: path.join(__dirname, 'dll', '[name]-manifest.json'),
      name: '[name]',
    }),
  ]
};

複製代碼

webpack.config.prod.js片斷react

const manifest = require('./dll/vendor-manifest.json');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
...
plugins: [
    // webpack讀取到vendor的manifest文件對於vendor的依賴不會進行編譯打包
    new webpack.DllReferencePlugin({
      manifest,
    }),
    // 往html中注入vendor js
    new AddAssetHtmlPlugin([{ 
      publicPath: "/view/static/js",  // 注入到html中的路徑
      outputPath: "../build/static/js", // 最終輸出的目錄
      filepath: path.resolve(__dirname, './dist/*.js'),
      includeSourcemap: false,
      typeOfAsset: "js"
    }]),
]
複製代碼

js並行編譯與壓縮

webpack對文件的編譯處理是單進程的,但實際上咱們的編譯機器一般是多核多進程,若是能夠充分利用cpu的運算力,能夠提高很大的編譯速度。webpack

解決方案:使用happypack進行多進程構建,使用webpack4內置的TerserPlugin並行模式進行js的壓縮。ios

說明:happypack原理可參考http://taobaofed.org/blog/2016/12/08/happypack-source-code-analysis/git

webpack.config.prod.js片斷github

const HappyPack = require('happypack');
// 採用多進程,進程數由CPU核數決定
const happThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
...
optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          parse: {
            ecma: 8,
          },
          compress: {
            ecma: 5,
            warnings: false,
            comparisons: false,
            inline: 2,
          },
          mangle: {
            safari10: true,
          },
          output: {
            ecma: 5,
            comments: false,
            ascii_only: true,
          },
        },
        parallel: true,
        cache: true,
        sourceMap: false,
      }),
    ]
},
module: {
    rules: [
      {
        test: /.css$/,
        oneOf: [
          {
            test: /\.(js|mjs|jsx)$/,
            include: paths.appSrc,
            loader: 'happypack/loader',
            options: {
              cacheDirectory: true,
            },
          },
        ]
      }
    ]
},
plugins: [
    new HappyPack({
      threadPool: happThreadPool,
      loaders: [{
        loader: 'babel-loader',
      }]
    }),
]
複製代碼

體積篇

按需加載

當js頁面特別多的時候,若是都打包成一個文件,那麼很影響訪問頁面訪問的速度。理想的狀況下,是到相應頁面的時候才下載相應頁面的js。

解決方案:使用import('path/to/module') -> Promise。調用 import() 之處,被做爲分離的模塊起點,意思是,被請求的模塊和它引用的全部子模塊,會分離到一個單獨的 chunk 中。

說明: 老版本使用require.ensure(dependencies, callback)進行按需加載,webpack > 2.4 的版本此方法已經被import()取代

通常例子

按需加載demo,在非本地的環境下開啓監控上報

if (process.env.APP_ENV !== 'local') {
  import("./utils/emonitor").then(({emonitorReport}) => {
    emonitorReport();
  });
}
複製代碼

react例子

react頁面按需加載,可參考http://react.html.cn/docs/code-splitting.html,裏面提到的React.lazy,React.Suspense是在react 16.6版本以後纔有的新特性,對於老版本,官方依然推薦使用react-loadable實現路由懶加載

react-loadable示例

import React, { Component } from 'react';
import { Route, Switch } from 'react-router-dom';
import Loadable from 'react-loadable';
import React, { Component } from 'react';
// 通過包裝的組件會在訪問相應的頁面時才異步地加載相應的js
const Home = Loadable({
  loader: () => import('./page/Home'),
  loading: (() => null),
  delay: 1000,
});
import NotFound from '@/components/pages/NotFound';

class CRouter extends Component {

  render() {
    return (
      <Switch>
          <Route exact path='/' component={Home}/>
          {/* 若是沒有匹配到任何一個Route, <NotFound>會被渲染*/}
          <Route component={NotFound}/>
      </Switch>
    )
  }
}

export default CRouter

複製代碼

vue例子

vue頁面按需加載,可參考https://router.vuejs.org/zh/guide/advanced/lazy-loading.html

示例

// 下面2行代碼,沒有指定webpackChunkName,每一個組件打包成一個js文件。
const ImportFuncDemo1 = () => import('../components/ImportFuncDemo1')
const ImportFuncDemo2 = () => import('../components/ImportFuncDemo2')
// 下面2行代碼,指定了相同的webpackChunkName,會合並打包成一個js文件。
// const ImportFuncDemo = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/ImportFuncDemo')
// const ImportFuncDemo2 = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/ImportFuncDemo2')
export default new Router({
    routes: [
        {
            path: '/importfuncdemo1',
            name: 'ImportFuncDemo1',
            component: ImportFuncDemo1
        },
        {
            path: '/importfuncdemo2',
            name: 'ImportFuncDemo2',
            component: ImportFuncDemo2
        }
    ]
})
複製代碼

css預加載

作完按需加載以後,假如定義的分離點裏包含了css文件,那麼相關css樣式也會被打包進js chunk裏,並經過URL.createObjectURL(blob)的方式加載到頁面中。

假如n個頁面引用了共同的css樣式,無形中也增長n倍的 css in js體積。經過css預加載,把共同css提煉到html link標籤裏,能夠優化這部分的體積。

解決方案:把分離點裏的頁面css引用(包括less和sass)提煉到index.less中,在index.js文件中引用。假如使用到庫的less文件特別多,能夠定義一個cssVendor.js,在index.js中引用,並在webpack config中添加一個entry以配合MiniCssExtractPlugin作css抽離。

P.S. 假如用到antd或其餘第三方UI庫,按需加載的時候記得把css引入選項取消,把 style: true選項刪掉

示例

cssVendor片斷

// 全局引用的組件的樣式預加載,按需引用,可優化異步加載的chunk js體積
// Row
import 'antd/es/row/style/index.js';
// Col
import 'antd/es/col/style/index.js';
// Card
import 'antd/es/card/style/index.js';
// Icon
import 'antd/es/icon/style/index.js';
// Modal
import 'antd/es/modal/style/index.js';
// message
import 'antd/es/message/style/index.js';
...
複製代碼

webpack.config.production片斷

entry:
   {
      main: [paths.appIndexJs, paths.cssVendorJs]
   },
  plugins: [
    new HappyPack({
      threadPool: happThreadPool,
      loaders: [{
        loader: 'babel-loader',
        options: {
          customize: require.resolve(
            'babel-preset-react-app/webpack-overrides'
          ),
          plugins: [
            [
              require.resolve('babel-plugin-named-asset-import'),
              {
                loaderMap: {
                  svg: {
                    ReactComponent: '@svgr/webpack?-prettier,-svgo![path]',
                  },
                },
              },
            ],
            ['import',
              { libraryName: 'antd', libraryDirectory: 'es' },
            ],
        
          ],
          cacheDirectory: true,
          cacheCompression: true,
          compact: true,
        },
      }],
    })]
複製代碼

按需打包

咱們在項目的開發中常常會引用一些第三方庫,例如antd,lodash。這些庫在咱們的項目中默認是全量引入的,但其實咱們只用到庫裏的某些組件或者是某些函數,那麼按需只打包咱們引用的組件或函數就能夠減小js至關大一部分的體積。

解決方案:使用babel-plugin-import插件來實現按需打包,具體使用方式可參考https://github.com/ant-design/babel-plugin-import

示例

{
    test: /\.(js|jsx)$/,
    include: paths.appSrc,
    loader: require.resolve('babel-loader'),
    exclude: /node_modules/,
    options: {
      plugins: [
        ['import', [
          { libraryName: 'lodash', libraryDirectory: '', "camel2DashComponentName": false,  },
          { libraryName: 'antd', style: true }
        ]
        ],
      ],
      compact: true,
    },
}
複製代碼

忽略沒必要要的文件

有些包含多語言的庫會將全部本地化內容和核心功能一塊兒打包,因而打包出來的js裏會包含不少多語言的配置文件,這些配置文件若是不打包進來,也能夠減小js的體積。

解決方案:使用IgnorePlugin插件忽略指定資源路徑的打包

示例

new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
複製代碼

壓縮

壓縮是一道常規的生產工序,前端項目編譯出來的文件通過壓縮混淆,能夠把體積進一步縮小。

解決方案:使用TerserPlugin插件進行js壓縮,使用OptimizeCSSAssetsPlugin插件css壓縮

說明:webpack4以前js壓縮推薦使用ParalleUglifyPlugin插件,它在UglifyJsPlugin的基礎上作了多進程並行處理的優化,速度更快;css壓縮推薦使用cssnano,它基於PostCSS。由於css-loader已經將其內置了,要開啓cssnano去壓縮代碼只須要開啓css-loader的minimize選項。

示例

minimizer: [
  new TerserPlugin({
    terserOptions: {
      parse: {
        ecma: 8,
      },
      compress: {
        ecma: 5,
        warnings: false,
        comparisons: false,
        inline: 2,
      },
      mangle: {
        safari10: true,
      },
      output: {
        ecma: 5,
        comments: false,
        ascii_only: true,
      },
    },
    parallel: true,
    cache: true,
    sourceMap: shouldUseSourceMap,
  }),
  new OptimizeCSSAssetsPlugin({
    cssProcessorOptions: {
      parser: safePostCssParser,
      map: shouldUseSourceMap
        ? {
          inline: false,
          annotation: true,
        }
        : false,
    },
  }),
]
複製代碼

抽離共同文件

在不少chunks裏,有相同的依賴,把這些依賴抽離爲一個公共的文件,則能夠有效地減小資源的體積,並能夠充分利用瀏覽器緩存。

解決方案:使用SplitChunksPlugin抽離共同文件

P.S. webpack4使用SplitChunksPlugin代替了CommonsChunkPlugin 示例

optimization: {
  splitChunks: {
    chunks: 'all',
    name: false
  }
}

複製代碼

SplitChunksPlugin的具體配置可參考 juejin.im/post/5af15e…

開啓Scope Hoisting(做用域提高)

Scope Hoisting 是webpack3中推出的新功能,能夠把依賴的代碼直接注入到入口文件裏,減小了函數做用域的聲明,也減小了js體積和內存開銷

舉個栗子 假如如今有兩個文件分別是 util.js:

export default 'Hello,Webpack';
複製代碼

和入口文件 main.js:

import str from './util.js';
console.log(str);
複製代碼

以上源碼用 Webpack 打包後輸出中的部分代碼以下:

[
  (function (module, __webpack_exports__, __webpack_require__) {
    var __WEBPACK_IMPORTED_MODULE_0__util_js__ = __webpack_require__(1);
    console.log(__WEBPACK_IMPORTED_MODULE_0__util_js__["a"]);
  }),
  (function (module, __webpack_exports__, __webpack_require__) {
    __webpack_exports__["a"] = ('Hello,Webpack');
  })
]

複製代碼

在開啓 Scope Hoisting 後,一樣的源碼輸出的部分代碼以下:

[
  (function (module, __webpack_exports__, __webpack_require__) {
    var util = ('Hello,Webpack');
    console.log(util);
  })
]
複製代碼

從中能夠看出開啓 Scope Hoisting 後,函數申明由兩個變成了一個,util.js 中定義的內容被直接注入到了 main.js 對應的模塊中。

解決方案:webpack4 production mode會自動開啓ModuleConcatenationPlugin,實現做用域提高。

Tree Shaking

tree shaking 是一個術語,一般用於描述移除 JavaScript 上下文中的未引用代碼(dead-code)

有的時候,代碼裏或者引用的模塊裏包含裏一些沒被使用的代碼塊,打包的時候也被打包到最終的文件裏,增長了體積。這種時候,咱們可使用tree shaking技術來安全地刪除文件中未使用的部分。

使用方法:

  • 使用 ES2015 模塊語法(即 import 和 export)。
  • 在項目 package.json 文件中,添加一個 "sideEffects" 屬性。
  • 引入一個可以刪除未引用代碼(dead code)的壓縮工具(minifier)(例如 UglifyJSPlugin)。

分析工具

在體積優化的路上,咱們可使用工具來分析咱們打包出的體積最終優化成怎樣的效果。 經常使用的工具備兩個:

  • webpack-bundle-analyzer 使用須要在webpack.config.js中配置
plugins: [
  new BundleAnalyzerPlugin()
]
複製代碼

執行完build後會打開網頁,效果圖以下:

  • source-map-explorer source-map-explorer是根據source-map去分析出打包文件的體積大小,在本地調試是時設置 devtool: true,而後執行source-map-explorer build/static/js/main.js則能夠去分析指定js的體積。效果圖以下:
相關文章
相關標籤/搜索