大聲對webpack4.0說聲你好之webpack的高級應用(四)

導讀

再學了前三節以後,其實咱們已經會對文件資源等進行一些打包,可是這些在大型項目中是遠遠不夠的,那麼咱們在平時的配置中還會遇到什麼難題呢?css

我能學會什麼

經過本節的學習,你能夠學會html

  1. 按需打包文件
  2. 區分線上環境與開發環境配置
  3. Webpack 和 Code Splitting
  4. SplitChunksPlugin 配置參數詳解
  5. Lazy loading/chunk
  6. 打包分析流程
  7. webpack與瀏覽器緩存問題
  8. css分割
  9. 瀏覽器緩存
  10. Shimming
  11. 環境變量

具體應用

  1. 根據import引入的代碼按需打包,避免form的文件總體打包,我引入什麼你打包什麼
  2. 根據本身的需求自行配置線上與開發環境的配置,拆分公共配置代碼,使用自定義命令一鍵打包代碼
  3. 代碼分割,同步加載與異步加載的配置
  4. SplitChunksPlugin經常使用配置詳解
  5. 懶加載例子與chunk介紹
  6. 簡單打包分析,preloading,prefetching
  7. 再也不將css混淆打包,而是在dist目錄下生成一個css文件夾,而後打包進去
  8. js文件在瀏覽器中緩存問題,打包解決方式
  9. Shimming做用
  10. 環境變量的配置與使用

Tree Shaking

接上一講,若是咱們在preset-env設置了"useBuiltIns": "usage",那麼實際上咱們不去引入babel/polyfill也是能夠的。由於咱們在使用useBuiltIns,它會自動幫咱們引入,因此這節咱們直接能夠寫es6語法。vue

新建一個math.js,而後咱們在 m.js中引入,自行修改打包配置文件,若是你還不會請點擊3分鐘瞭解webapcknode

export const add = (a, b) => {
  return a + b
}

export const minus = (a, b) => {
  return a - b 
}

// m.js
import { add } from './math'

console.log(add(1, 3))
複製代碼

這個時候咱們雖然實現了效果,可是在打包文件中,我卻將個人math文件徹底打包了。這裏我卻只引入了add方法,因此我是但願他只打包我引入的文件。因此在package.json中能夠作如下配置。jquery

"sideEffects": fasle
複製代碼

須要注意的是,這個在線上環境纔有用,由於他在開發中會方便咱們去調試。webpack

區分線上環境與開發環境配置

咱們爲何要這麼作

每次打包(線上,開發)代碼以前,咱們都會去不斷修改webpack.config.js中的文件,例如modo,插件等之類的,這樣的操做是很麻煩的,並且咱們也不可能100%保證咱們就不會改錯文件,畢竟改錯了文件的影響是很是大的,接下來咱們一塊兒看看若是區分線上與開發環境的配置。git

拆分dev與prod文件

咱們以前有一個webpack.config.js,咱們將其重命名爲webpack.dev.js,而後複製一份改名webpack.prod.js。而後根據須要更新以下兩個文件。es6

// webpack.dev.js

const path = require('path'); // 從nodejs中引入path變量
const htmlPlugin = require('html-webpack-plugin'); // 引入html打包插件
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 
const webpack = require('webpack') // 引入webpack插件

module.exports = {
  mode: 'development',
  devtool: 'cheap-module-eval-source-map',
  entry: {
    main: './src/m.js',
  },
  devServer: {
    contentBase: './dist', // 藉助webpack啓動服務器,根目錄就是打包以後的dist文件夾
    open: true, // 啓動npm run start的時候自動打開瀏覽器
    proxy: { // 配置代理
      '/api': 'http://localhost:3000' 
    },
    port: 8080, // 配置端口號
    hot: true, // 開啓熱更新
    //hotOnly: true // 就算是html文件沒生效也不刷新頁面
  },
  module: { // 模塊打包配置
    // ... dev和prod同樣 不寫了
  },
  plugins: [
    new htmlPlugin({
      template: './index.html'
    }),
    new CleanWebpackPlugin(),
    new webpack.HotModuleReplacementPlugin() // 引入插件
  ],
  output: {
    publicPath: '/',
    filename: 'dist.js',  // 打包後生成的main.js
    path: path.resolve(__dirname, 'dist'), // 打包到dist文件夾
  }
}

// webpack.prod.js
const path = require('path'); // 從nodejs中引入path變量
const htmlPlugin = require('html-webpack-plugin'); // 引入html打包插件
const cleanPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'production',
  devtool: 'cheap-module-source-map',
  entry: {
    main: './src/m.js',
  },
  module: { // 模塊打包配置
    // ...
  },
  plugins: [
    new htmlPlugin({
      template: './index.html'
    }),
    new cleanPlugin(['dist']),
  ],
  output: {
    publicPath: '/',
    filename: 'dist.js',  // 打包後生成的main.js
    path: path.resolve(__dirname, 'dist'), // 打包到dist文件夾
  }
}
複製代碼

ok,文件以及拆分了,這個時候咱們會修改package.json裏面的scriptsgithub

"scripts": {
    "dev": "webpack-dev-server --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js"
  },
複製代碼

重啓服務,打包文件正常運行。這樣咱們就區分開了,可是有一個很明顯的問題就是,這兩個文件,重複的地方太多了,若是我之後新增了一個公共的代碼,兩個文件都要加,刪除也是兩個文件都要作。這樣就會讓個人維護成本變高,並且仍是會增長錯誤的概率,因此咱們有必要對配置文件進行合併。web

合併公共配置文件

先下載webapck-merge,他能夠幫助咱們合併webpack的配置

npm install webpack-merge -D
複製代碼

新建webpack.common.js進行代碼合併

  • entry同樣,提取出來。
  • module同樣,提取出來。
  • plugins有兩個公共插件,提出出來。
  • output同樣,提取出來
// webpack.common.js
const path = require('path'); // 從nodejs中引入path變量
const htmlPlugin = require('html-webpack-plugin'); // 引入html打包插件
const cleanPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    main: './src/m.js',
  },
  module: { // 模塊打包配置
    // ... 省略
  },
  plugins: [
    new htmlPlugin({
      template: './index.html'
    }),
    new cleanPlugin(['dist']),
  ],
  output: {
    publicPath: '/',
    filename: 'dist.js',  // 打包後生成的main.js
    path: path.resolve(__dirname, 'dist'), // 打包到dist文件夾
  }
}

// webpck.dev.js
const webpack = require('webpack') // 引入webpack插件
const webpackMerge = require('webpack-merge')
const commonConfig = require('./webpack.common')

const devConfig = {
  mode: 'development',
  devtool: 'cheap-module-eval-source-map',
  devServer: {
    contentBase: './dist', // 藉助webpack啓動服務器,根目錄就是打包以後的dist文件夾
    open: true, // 啓動npm run start的時候自動打開瀏覽器
    proxy: { // 配置代理
      '/api': 'http://localhost:3000' 
    },
    port: 8080, // 配置端口號
    hot: true, // 開啓熱更新
    //hotOnly: true // 就算是html文件沒生效也不刷新頁面
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin() // 引入插件
  ]
}

module.exports =  webpackMerge(commonConfig, devConfig)

// webpack.prod.js
const webapckMerge = require('webpack-merge')
const commonConfig = require('./webpack.common')

const prodConfig = {
  mode: 'production',
  devtool: 'cheap-module-source-map',
}

module.exports = webapckMerge(commonConfig, prodConfig)
複製代碼

npm run dev,ok nice.

Code Splitting

代碼分割,這個我就舉例說明一下就ok。

在咱們平時使用vue等大框架的時候,常常會用到一個lodash.js,假設咱們正常的下載並使用改代碼。

文件 大小
lodash.js 1MB
axin.js 1MB
// axin.js
import _ form 'lodash'

// 使用lodash
複製代碼

這樣的話,假設咱們的代碼不作壓縮,咱們的代碼就會達到2MB大小,若是用戶打開咱們的網頁,這個時候咱們就會先去加載這個2mb的文件,這樣的話,對用戶體驗很很差。那若是咱們可以達到以下效果,就好多了 。

// lo.js
import _ form 'lodash'
window._ = _

// axin.js
// 使用lodash.js
複製代碼

js是支持並行加載的,不能說必定比2m的快,可是至少能優化很多,最大的好處是什麼?是咱們若是隻是修改了axin.js的內容,那咱們的lo.js是不須要改變的,瀏覽器中會有緩存,這個時候想要的效果就會明顯提高。

那麼咱們在webpack中應該如何配置呢?找到webpack.common.js

optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },
複製代碼

這個時候就會幫咱們去拆分代碼,須要特別說明的是,webpack和Code Splitting是沒有關係的,默認的會幫咱們下載一個功能,咱們只須要配置便可。

這個是同步加載的方式,有時候咱們的文件是異步回來的,其實也是這麼一回事。我就很少作演示。 你們有興趣的能夠本身下來試試。

SplitChunksPlugin

爲了搞清楚,這個插件,仍是沒能逃避寫一個異步加載的方法來使用組件。

function getComponent(){
  return import('lodash').then(({ default: _}) => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['jsxin', 'hello'], '-')
    return element
  })
}

getComponent().then(element => {
  document.body.appendChild(element)
})
複製代碼

// {default: } 加載回來的賦值給_

不管是同步加載或者異步,咱們都會進行代碼分割。咱們先來下載一個官方提供的動態引入的插件。

平常直通車:babeljs.io/docs/en/nex…

npm install --save-dev @babel/plugin-syntax-dynamic-import

// .babelrc
{
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}

//package.json 
"dev-build": "webpack --config webpack.dev.js",

複製代碼

webpack-dev-server會把文件寫到內存咱們是觀察不到的,因此新增一個命令npm run dev-build,讓其打包代碼。

這時候給我生成了一個0.dist.js

咱們能夠在引入以前使用註釋符爲其設置名字

function getComponent(){
  return import(/* webpackChunkName: "loadash" */'lodash').then(({ default: _}) => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['jsxin', 'hello'], '-')
    return element
  })
}

複製代碼

就會生成一個vendors~lodash.dist.js

由於這裏設置的比較多,咱們簡單的把配置項講解一下,如下爲配置項,若是你的splitChunks沒有配置任何內容,就會使用如下的內容做爲配置項。

optimization: {
    splitChunks: {
      chunks: 'async', // all 不區分  async 只對異步代碼生效
      minSize: 30000, // 打包最小30000字節我纔去分割
      minRemainingSize: 0, 
      maxSize: 0, // 通常配置 50000 就至關於能拆分紅幾個50kb左右的
      minChunks: 1, // 最少使用一次
      maxAsyncRequests: 6, // 同時加載的模塊數最多6個
      maxInitialRequests: 4, // 入口文件也會拆分 可是最多4個 超過了就不分分割了
      automaticNameDelimiter: '~',  // 名字和組的拼接符 vendors~lodash
      cacheGroups: { // 拆分分組
        defaultVendors: { // 默認分組
          test: /[\\/]node_modules[\\/]/, // 若是是node_modules中的咱們就到defaultVendors這個組
          priority: -10, // 優先級, 和下面default 同時知足條件 打包到優先級高的裏面
          // filename: 'vendor.js' 能夠本身取名字
        },
        default: {
          minChunks: 2,
          priority: -20, 
          reuseExistingChunk: true // 好比以前引用了a代碼,就不會打包a到common.js,會複用
          // filename: 'common.js' 能夠本身取名字
        }
      }
    }
  },
複製代碼

Lazy Loading

懶加載

咱們對剛纔的異步代碼作一點改進

function getComponent(){
  return import(/* webpackChunkName: "loadsh" */'lodash').then(({ default: _}) => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['jsxin', 'hello'], '-')
    return element
  })
}

document.addEventListener('click', () => {
  getComponent().then(element => {
    document.body.appendChild(element)
  })
})
複製代碼

咱們仍是異步加載一個loadsh函數,而後在頁面中綁定了一個點擊事件,只有咱們監聽到點擊事件的時候,咱們纔回去調用getComponent方法,而後經過getComponent方法去引入loadsh函數。

效果就是咱們在頁面中,開始只會加載一個main.js,而後點擊一下頁面會在加載一個loadsh函數,調用這個函數的某些方法咱們實現了一個字符串的拼接過程,最終呈如今了頁面上。

經過import方法,咱們只有訪問了在某些文件的時候,他纔會異步加載,而後執行。這樣咱們加載速度也會更快。

固然後也可使用es7中比較流行的async來處理這個時間,讓大家的代碼更加直爽。

async function getComponent() {
    const { default: _ } = await import(/* webpackChunkName: "loadsh" */'lodash')
    const element = document.createElement('div')
    element.innerHTML = _.join(['jsxin', 'hello'], '-')
    return element
}

複製代碼

chunk

咱們在以前已經使用了不少次chunk了,那麼咱們這個chunk究竟是什麼?

在js代碼打包中,咱們會拆分紅多個js文件,那麼每個js文件,咱們都稱它爲一個chunk。

打包分析,preloading, prefetching

打包分析

先來看看官方的webpack分析工具

若是你相對咱們打包以後的代碼進行分析,首先你須要將--profile --json > stats.json 放到你打包的命令中

"dev-build": "webpack --profile --json > stats.json --config webpack.dev.js",
複製代碼

他的意思就是將個人打包過程放到stats.json這個文件中。

他會將咱們整個打包的流程都寫進入,比較耗時,打包了什麼資源,有幾個模塊,幾個chunk等,你能夠能夠藉助官方工具幫你翻譯一下。這裏你們能夠了解一下,我就很少作介紹,能夠自行打包嘗試。

preloading

在這個知識點以前咱們先來看看咱們最原始的代碼寫法。

document.addEventListener('click', () => {
  const element = document.createElement('div')
  element.innerHTML = 'jsxin'
  document.body.appendChild(element)
})
複製代碼

這個是咱們經常使用的標準寫法,難道這個寫法就沒有優化空間了嗎?

編譯成功以後咱們打開f12, 而後按住command+shift+p,輸入coverage這個關鍵詞

咱們點擊一下show Coverage,而後左側會出現一個錄製按鈕,咱們會發現咱們的main.js只有75%的代碼使用率。

爲何呢?由於咱們在頁面加載的使用,咱們並不會使用

const element = document.createElement('div')
element.innerHTML = 'jsxin'
document.body.appendChild(element)
複製代碼

這些代碼是在被點擊的時候纔會用到,因此這不是webpack推薦的一種書寫代碼的方式。

咱們能夠將這部分代碼這麼寫,新建click.js

// click.js
export default function addComponent () {
  const element = document.createElement('div')
  element.innerHTML = 'jsxin'
  document.body.appendChild(element)
}

// com.js
document.addEventListener('click', () => {
  import('./click.js').then(({ default: _ }) => {
    _()
  })
})

複製代碼

這樣的話,他的使用了就達到了79%,也會節約咱們的首屏加載時間。

prefetching

咱們一個網頁,剛開始初始化首頁的時候,咱們不加載登陸模態框,先加載首頁的其餘邏輯,等加載完成以後,帶寬被釋放出來了,咱們偷偷的加載登陸模態框,這樣的話,既知足了我首頁加載快的需求,又知足了登陸加載快的需求。

而這個方案就是咱們結合prefetching和preloading的一個比較實用的例子。

能夠在import以前聲明prefetching配置

import(/* webpackPrefetch: true */'./click.js')
複製代碼

這個時候,等他將咱們的核心代碼加載完成以後,就會偷偷的加載click.js

CSS代碼分割

場景

咱們生成以下代碼

import '../statics/style/index.css'

console.log(123)
複製代碼

我如今但願個人index.css不直接生成css代碼到個人頁面上,而是但願他在dist下面新建一個文件夾,而後把css放進去引入,那麼這麼時候咱們應該怎麼處理這種操做呢?

插件介紹

官方插件:webpack.js.org/plugins/min…

特別說明:適合線上環境中使用,由於更新以後不會自動刷新

先來安裝一下插件

npm install --save-dev mini-css-extract-plugin
複製代碼

而後在線上環境中使用。

插件配置

// webpack.prod.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

plugins: [new MiniCssExtractPlugin()],
複製代碼

而後咱們以前使用的style-loader就不能用了,他給咱們提供了一個loader,咱們將style-loader替換成他的loader,而後還要將css區分開線上與開發環境。

// webpack.prod.js
module: {
    rules: [{
      test: /\.css$/, // 檢測文件是css結尾的
      use: [MiniCssExtractPlugin.loader, 'css-loader']
    },
      {
        test: /\.scss$/, // 檢測文件是scss結尾的
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 2, // 經過import引入的scss文件,也要走下面兩個loader
              // modules: true
            }
          },
          'sass-loader',
          'postcss-loader'
        ]
      }]
  }
複製代碼

而後咱們打包一個線上的代碼試試。就在咱們的代碼中生成了main.css文件。

配置項

咱們簡單的看看他的配置項

plugins: [new MiniCssExtractPlugin({
    filename: '[name].css',
    chunkFilename: '[name].chunk.css'
  })],
複製代碼

若是樣式是直接被引用,他就會走filename,間接就是chunkfilename

咱們不妨再來作一些嘗試。

// index.css
.avatar{
  width: 100px;
  height: 100px;
}

// index1.css
.avatar{
  display: block;
}

// style.css
import '../statics/style/index.css'
import '../statics/style/index1.css'

console.log(123)
複製代碼

咱們再次打包,你會發現,他自動將兩個樣式文件給我合併到main.css中了。

// 打包後
.avatar{
  width: 100px;
  height: 100px;
}
.avatar{
  display: block;
}

/*# sourceMappingURL=main.css.map*/
複製代碼

ok,那麼咱們若是還想對這個css進行一些壓縮怎麼辦呢?

optimize-css-assets-webpack-plugin

// install
npm install optimize-css-assets-webpack-plugin -D

// prod
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
optimization: {
    minimizer: [new OptimizeCSSAssetsPlugin({})]
  }
複製代碼

而後在打包,他不只將咱們的代碼進行了壓縮,還將咱們的代碼合併到了一塊兒。

// 打包後
.avatar{width:100px;height:100px;display:block}
複製代碼

ok,其餘更高級的用法,請參考官方文檔,這裏一方面是和你們一塊兒體驗,另外一方面是推薦經常使用的插件。

瀏覽器緩存

咱們在加載一個網頁的時候,咱們可能首先會加載一個index.html,和兩個js文件,當你下次訪問的時候,其實瀏覽器已經對你兩個js有緩存了,這個時候會優先讀取緩存中的文件。

這個時候要麼你更改一下文件的名字,要麼就強制刷新,可是你確定不能讓用戶強制刷新頁面。因此咱們在調試過程當中能夠無論,從新配置一下output

// cache.sj
import _ from 'lodash'

let str = _.join(['j', 's', 'x', 'i', 'n'], '-')
console.log(str)


output: {
    filename: '[name].[contenthash].js', 
  }
複製代碼

contenthash就是咱們根據內容生成的一個hash值,只要你的內容沒有改變,那麼咱們就不用從新去加載這些js。

咱們改變的是什麼?咱們會修改本身的邏輯源代碼,可是你並不會去改變node_modules的第三方代碼,因此這些東西確定仍是可讓瀏覽器讀緩存,提升網站加載效率。

Shimming

webpack是基於模塊打包的,也就是說咱們在一個模塊裏面的代碼,到另一個模塊就找不到了。

// jq.js
import $ from 'jquery'
import { jqui } from './jq.ui'

jqui()
$('body').append('<div>axin</div>')

// jq.ui.js
export function jqui(){
  $('body').css('background','red')
}

// $ is not defined
複製代碼

webpack是提供了一下插件的,咱們來看看他是幹嗎的。

new webpack.ProvidePlugin({
  $: 'jquery'
})
複製代碼

他會去檢測你哪一個文件是使用了$符,若是使用了,那麼你是否在上面引入了jquery,若是沒有的話,他就會自動幫你在上面引入,很是nice。

還記得咱們使用了一個babel/polyfill嗎?若是你沒有promise等,他會幫你完成promise的實現。

環境變量

咱們以前是在dev/prod環境中合併的common,那麼這裏咱們來使用一個環境變量來從新合併咱們的打包配置文件,先上代碼。

// dev/prod 註釋如下代碼 而後分包導出
const webapckMerge = require('webpack-merge')
const commonConfig = require('./webpack.common')

module.exports = prodConfig
module.exports = devConfig

// webpack.common.js
const webapckMerge = require('webpack-merge')
const prodConfig = require('./webpack.prod')
const devConfig = require('./webpack.dev')

const commonConfig = ...(配置)

module.exports = (env) => {
  if(env && env.production){
    return webapckMerge(prodConfig, commonConfig)
  }
  return webapckMerge(devConfig, commonConfig)
}
複製代碼

這裏咱們直接導出了一個函數,接收了一個env,因此咱們在打包腳本中這麼這麼寫

"scripts": {
    "dev-build": "webpack --profile --json > stats.json --config webpack.common.js",
    "dev": "webpack-dev-server --config webpack.common.js",
    "build": "webpack --env.production --config webpack.common.js"
  },
複製代碼

配置了env變量,咱們在打包的時候都是webpack.common.js,而後判斷就能夠不一樣的配置進行打包了。

ok,就到這裏吧,知識點即總結,若是你也想和我一塊兒學習webpack,我們第五節見。

相關文章
相關標籤/搜索