【webpack 系列】進階篇

本文將繼續引入更多的 webpack 配置,建議先閱讀【webpack 系列】基礎篇的內容。若是發現文中有任何錯誤,請在評論區指正。本文全部代碼均可在 github 找到。css

打包多頁應用

以前咱們配置的是一個單頁的應用,可是咱們的應用可能須要是個多頁應用。下面咱們來進行多頁應用的 webpack 配置。 先看一下咱們的目錄結構html

├── public
│   ├── detail.html
│   └── index.html
├── src
│   ├── detail-entry.js
│   ├── index-entry.js
複製代碼

public 下面有 index.htmldetail.html 兩個頁面,對應 src 下面有 index-entry.jsdetail-entry.js 兩個入口文件。前端

webpack.config.js 配置node

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
// ...

module.exports = {
  entry: {
    index: path.resolve(__dirname, 'src/index-entry.js'),
    detail: path.resolve(__dirname, 'src/detail-entry.js')
  },
  output: {
    path: path.resolve(__dirname, 'dist'), // 輸出目錄
    filename: '[name].[hash:6].js', // 輸出文件名
  },
  plugins: [
    // index.html
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'public/index.html'), // 指定模板文件,不指定會生成默認的 index.html 文件
      filename: 'index.html', // 打包後的文件名
      chunks: ['index'] // 指定引入的 js 文件,對應在 entry 配置的 chunkName 
    }),
    // detail.html
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'public/detail.html'), // 指定模板文件,不指定會生成默認的 index.html 文件
      filename: 'detail.html', // 打包後的文件名
      chunks: ['detail'] // 指定引入的 js 文件,對應在 entry 配置的 chunkName
    }),
    // 打包前自動清除dist目錄
    new CleanWebpackPlugin()
  ]
}
複製代碼

npm run build 以後能夠看到生成的 dist 目錄以下webpack

dist
├── assets
│   └── author_ee489e.jpg
├── detail.dbcb15.js
├── detail.dbcb15.js.map
├── detail.html
├── index.dbcb15.js
├── index.dbcb15.js.map
└── index.html
複製代碼

index.html 頁面中已經引入了打包好的 index.dbcb15.js 文件,detail.html 文件也已經引入了 detail.dbcb15.js 文件。更多配置請查看 html-webpack-plugingit

將 CSS 樣式單獨抽離生成文件

webpack4css 模塊支持的完善以及在處理 css 文件提取的方式上也作了些調整,由 mini-css-extract-plugin 來代替以前使用的 extract-text-webpack-plugin,使用方式很簡單。es6

該插件將 css 提取到單獨的文件中,爲每一個包含 cssjs 文件建立一個 css 文件,支持 csssourcemap 的按需加載。 與 extract-text-webpack-plugin 相比有以下優勢github

  1. 異步加載
  2. 沒有重複的編譯(性能)
  3. 更容易使用
  4. 特定於 css

安裝 extract-text-webpack-pluginweb

npm i -D mini-css-extract-plugin
複製代碼

配置 webpack.config.jsnpm

// webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// ...

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.(c|le)ss$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'],
        exclude: /node_modules/
      },
      {
        test: /\.sass$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'],
        exclude: /node_modules/
      },
      // ...
    ]
  },
  plugins: [
    // ...
    new MiniCssExtractPlugin({
      filename: 'css/[name].[hash:6].css'
    })
  ]
}
複製代碼

npm run build 以後會發如今 dist/css 目錄有了抽離出來的 css 文件了。

這時咱們發現兩個問題:

  1. 打包生成的 css 文件沒有進行壓縮。
  2. 全部文件命名的 hash 部分都是同樣的,存在緩存問題。

對 css 文件進行壓縮

經過 optimize-css-assets-webpack-plugin 插件壓縮 css 代碼

npm i -D optimize-css-assets-webpack-plugin
複製代碼

配置 webpack.config.js

// webpack.config.js
//...
const OptimizeCssPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
  //...
  plugins: [
    //...
    new OptimizeCssPlugin()
  ]
}
複製代碼

這樣就能夠對 css 文件進行壓縮了。

對於第二個問題,咱們首先須要瞭解下 hashchunkHashcontentHash 的區別。

hash、chunkhash、contenthash 的區別和使用

hash

hash 是基於整個 module identifier 序列計算獲得的,webpack 默認爲給各個模塊分配一個 id 以做標識,用來處理模塊之間的依賴關係,默認的 id 命名規則是根據模塊引入的順序賦予一個整數(123...)。任意修改、增長、刪除一個模塊的依賴,都會對整個 id 序列形成影響,從而改變 hash 值。也就是每次修改或者增刪任何一個文件,全部文件名的 hash 值都將改變,整個項目的文件緩存都將失效。

output: {
  path: path.resolve(__dirname, 'dist'), // 輸出目錄
  filename: '[name].[hash:6].js', // 輸出文件名
}

new MiniCssExtractPlugin({
  filename: 'css/[name].[hash:6].css'
})
複製代碼

能夠看到打包後的 jscss 文件的 hash 值是同樣的,因此對於沒有發生改變的模塊而言,這樣作是不合理的。

固然能夠看到,對於圖片等資源該 hash 仍是能夠生成一個惟一值的。

chunkhash

chunkhash 根據不一樣的入口文件進行依賴文件解析、構建對應的 chunk,生成對應的哈希值。咱們將 filename 配置成 chunkhash 來看一下打包的結果。

output: {
  path: path.resolve(__dirname, 'dist'), // 輸出目錄
  filename: '[name].[chunkhash:6].js', // 輸出文件名
}

new MiniCssExtractPlugin({
  filename: 'css/[name].[chunkhash:6].css'
})
複製代碼

能夠看到此時打包以後的 index.jsdetail.jschunkhash 是不同的。可是會發現 index.jsindex.css 以及 detail.jsdetail.csschunkhash 是一致的,而且任意改動 js 或者 css 都會引發對應的 cssjs 文件的 chunkhash 的改變,這是不合理的。因此這裏抽離出來的 css 文件將使用 contenthash,來區分 css 文件和 js 文件的更新。

contenthash

contenthash 是針對文件內容級別的,只有你本身模塊的內容變了,那麼 hash 值才改變。

output: {
  path: path.resolve(__dirname, 'dist'), // 輸出目錄
  filename: '[name].[chunkhash:6].js', // 輸出文件名
}

new MiniCssExtractPlugin({
  filename: 'css/[name].[contenthash:6].css'
})
複製代碼

OK,能夠看到分離出來的 css 文件已經和入口文件的 hash 值區分開了。

如何使用

爲了實現理想的緩存,咱們通常這樣使用他們:

  1. JS 文件使用 chunkhash
  2. 抽離的 CSS 樣式文件使用 contenthash
  3. gif|png|jpe?g|eot|woff|ttf|svg|pdf 等使用 hash

按需加載

不少時候咱們並不須要在一個頁面中一次性加載全部的 js 或者 css 文件,而是應該是須要用到時纔去加載相應的 js 或者 css 文件。

import()

好比,如今咱們須要點擊一個按鈕纔會使用對應的 jscss 文件,須要 import() 語法:

// index-entry.js

import './index.sass';
//...
const handle = () => import('./handle');
const handle2 = () => import('./handle2');


document.querySelector('#btn').onclick = () => {
  handle().then(module => {
    module.handleClick();
  });

  handle2().then(module => {
    module.default();
  });
}
複製代碼
// handle.js

import './handle.css';

export function handleClick () {
  console.log('handleClick');
}
複製代碼
// handle2.js

export default function handleClick () {
  console.log('handleClick2');
}
複製代碼

npm run build 能夠看到,多了這 3 個文件,而且只有在咱們點擊該按鈕是纔會去加載這 3 個文件。

webpackChunkName

這些文件可能不太好區分,咱們能夠經過設置 webpackChunkName 來定義生成的文件名

// index-entry.js
const handle = () => import(/* webpackChunkName: "handle" */ './handle');
const handle2 = () => import(/* webpackChunkName: "handle2" */ './handle2');
複製代碼

咱們再將這些文件的 hash 長度設置爲 8 加以區分

// webpack.config.js
module.exports = {
    output: {
      path: path.resolve(__dirname, 'dist'), // 輸出目錄
      filename: '[name].[chunkhash:6].js', // 輸出文件名
      chunkFilename: '[name].[chunkhash:8].js'
    }
    // ...
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:6].css',
      chunkFilename: 'css/[name].[contenthash:8].css'
    }),
複製代碼

npm run build 以後查看

固然咱們也能夠將 handlehandle2 文件的 webpackChunkName 設置成同樣的,這樣這兩個文件將會打包在一塊兒生成一個文件,能夠減小請求數量。

熱更新( HMR, Hot Module Replacement )

開發過程當中,咱們但願在瀏覽器不刷新頁面的狀況下可以去加載咱們修改的代碼,來提升咱們的開發效率。咱們來看下如何配置:

  1. 打開 webpack-dev-server 的熱更新開關
  2. 使用 HotModuleReplacementPlugin 插件

HotModuleReplacementPlugin 插件是 Webpack 自帶的,在 webpack.config.js 直接配置

// webpack.config.js

module.exports = {
  devServer: {
    //...
    hot: true
  },
  plugins: [
    //...
    new webpack.HotModuleReplacementPlugin() // 熱更新插件
  ]
}
複製代碼

在入口文件添加

if (module && module.hot) {
    module.hot.accept()
}
複製代碼

這樣就完成了熱更新的配置,可是此時 webpack 打包卻報錯了。

搜了一下 相關的問題,在開發環境中咱們使用了 HotModuleReplacementPlugin 此時須要使用 hash 來輸出文件,使用 chunkhash 會致使 webpack 報錯,而生產環境則沒有問題。可是如今咱們只是經過 process.env.NODE_ENV 這個變量來區分環境,這顯然不是一個很好的方式。 咱們最好可以須要區分一下開發環境和生產環境的配置文件。

定義不一樣環境的配置

咱們能夠給不一樣的環境定義不一樣的配置文件,可是這些文件將會有大量類似的配置,這時咱們能夠這樣來定義文件:

  1. webpack.base.js:定義公共的配置
  2. webpack.dev.js:定義開發環境的配置
  3. webpack.prod.js:定義生產環境的配置

咱們能夠將一些公共的配置抽離到 webpack.base.js,而後在 webpack.dev.jswebpack.prod.js 進行對應環境的配置。咱們還須要經過 webpack-merge 來合併兩個配置文件。

安裝 webpack-merge

npm i -D webpack-merge
複製代碼

如今 webpack.dev.js 就是這樣的

// webpack.dev.js

const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const merge = require('webpack-merge');
const baseConfig = require('./webpack.config.base');

module.exports = merge(baseConfig, {
  mode: 'development',
  devtool: 'inline-source-map',
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    port: '9000', // 默認是8080
    compress: true, // 是否啓用 gzip 壓縮
    hot: true
  },
  output: {
    path: path.resolve(__dirname, 'dist'), // 輸出目錄
    filename: '[name].[hash:6].js', // 輸出文件名
    chunkFilename: '[name].[hash:8].js'
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[hash:6].css',
      chunkFilename: 'css/[name].[hash:8].css'
    }),
    new webpack.HotModuleReplacementPlugin() // 熱更新插件
  ]
});

複製代碼

同時須要在 package.json 中指定咱們的配置文件

// package.json

"scripts": {
  "dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.dev.js",
  "build": "cross-env NODE_ENV=production webpack --config webpack.config.pro.js"
},
複製代碼

這時咱們就很優雅的區分開不一樣環境的配置了。

拷貝靜態資源

有時候咱們須要在 html 中直接引用一個打包好的第三方插件庫,這個庫不須要經過 webpack 編譯。好比咱們 lib 目錄下有個 lib-a.js,須要在 public/index.html 中直接引用它。

<!-- public/index.html -->
<script src="/lib/lib-a.js"></script>
複製代碼

這時 build 以後會發現 dist 下是沒有 lib 目錄的,這時會找不到這個文件。這時咱們須要藉助 CopyWebpackPlugin 這個插件來幫助咱們把根目錄下的 lib 目錄拷貝到 dist 目錄下面。

首先安裝 CopyWebpackPlugin

npm i -D CopyWebpackPlugin
複製代碼

配置 webpack.config.js

// webpack.config.js

const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  //...
  plugins: [
    //...
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, 'lib'),
        to: path.resolve(__dirname, 'dist/lib')
      }
    ])
  ]
}
複製代碼

這時後運行 npm run build 就會發現,dist 目錄下已經有了 lib目錄及文件了。

更多的配置請查看copy-webpack-plugin

Resolve 配置

Webpack 在啓動後會從配置的入口模塊出發找出全部依賴的模塊,Resolve 配置 Webpack 如何尋找模塊所對應的文件。 Webpack 內置 JavaScript 模塊化語法解析功能,默認會採用模塊化標準里約定好的規則去尋找,但你也能夠根據本身的須要修改默認的規則。

alias

resolve.alias 配置項經過別名來把原導入路徑映射成一個新的導入路徑。 好比咱們在 index-entry.js 中引入 lib/lib-b.js,你可能須要這樣引入

import '../lib/lib-b.js';
複製代碼

而當目錄層級比較深時,這個相對路徑就會變得很差辨認了。這時咱們能夠配置 lib 的一個別名。

// webpack.config.js

module.exports = {
  //...
  resolve: {
    alias: {
      '@lib': path.resolve(__dirname, 'lib') // 爲lib目錄添加別名
    }
  }
}
複製代碼

這時不管你處於目錄的哪一個層級,你只須要這樣引入

import '@lib/lib-b.js';
複製代碼

extensions

若是在導入文件時沒有帶後綴名,webpack 會自動帶上後綴後去嘗試訪問文件是否存在。 resolve.extensions 用於配置在嘗試過程當中用到的後綴列表,默認是

extensions: ['.js', '.json']
複製代碼

就是說當遇到 import '@lib/lib-b'; 時,webpack 會先去尋找 @lib/lib-b.js 文件,若是該文件不存在就去尋找 @lib/lib-b.json 文件, 若是仍是找不到就報錯。

若是你想優先使用其餘後綴文件,好比 .ts 文件,能夠這樣配置

// webpack.config.js

module.exports = {
  //...
  resolve: {
    alias: {
      '@lib': path.resolve(__dirname, 'lib'), // 爲lib目錄添加別名
      extensions: ['.ts', '.js', '.json'] // 從左往右
    }
  }
}
複製代碼

這樣就會先去找 .ts 了。不過通常咱們會將高頻的後綴放在前面,而且數組不要太長,減小嚐試次數,否則會影響打包速度。

如今咱們引入 js 文件時能夠省略後綴名了。

modules

resolve.modules 配置 webpack 去哪些目錄下尋找第三方模塊,默認是隻會去 node_modules 目錄下尋找。若是項目中某個文件夾下的模塊常常被導入,不但願寫很長的路徑,好比 import '../../../components/link',那麼就能夠經過配置 resolve.modules 來簡化。

// webpack.config.js

module.exports = {
  //...
  resolve: {
    modules: ['./src/components', 'node_modules'] // 從左到右查找
  }
}
複製代碼

這時,你就能夠經過 import 'link' 引入了。

mainFields

有一些第三方模塊會針對不一樣環境提供幾份代碼。例如分別提供採用 es5es62 份代碼,這 2 份代碼的位置寫在 package.json 文件裏。

{
  "jsnext:main": "es/index.js",// 採用 ES6 語法的代碼入口文件
  "main": "lib/index.js" // 採用 ES5 語法的代碼入口文件
}
複製代碼

webpack 會根據 mainFields 的配置去決定優先採用那份代碼, mainFields 默認配置以下:

mainFields: ['browser', 'main']
複製代碼

假如你想優先採用 ES6 的那份代碼,能夠這樣配置:

mainFields: ['jsnext:main', 'browser', 'main']
複製代碼

enforceExtension

resolve.enforceExtension 若是配置爲 true,那麼全部導入語句都必需要帶文件後綴。

enforceModuleExtension

enforceModuleExtensionenforceExtension 做用相似,但 enforceModuleExtension 只對 node_modules下的模塊生效。 由於安裝的第三方模塊中大多數導入語句沒帶文件後綴,若是這時你配置了 enforceExtensiontrue,那麼就須要配置 enforceModuleExtension: false來兼容第三方模塊。

利用 webpack 解決跨域問題

本地開發時,前端項目的端口號是 9000,可是服務端多是 9001,根據瀏覽器的同源策略,是不能直接請求到後端服務的。固然你能夠在後端配置 CORS 相關的頭部來實現跨域,其實也能夠經過 webpack 的配置來解決跨域問題。

首先,咱們起一個後端服務,安裝 koakoa-router

npm i -D koa koa-router
複製代碼

新建 server/index.js

// server/index.js

const Koa = require('koa');
const KoaRouter = require('koa-router');

const app = new Koa();

// 建立 router 實例對象
const router = new KoaRouter();

// 註冊路由
router.get('/user', async (ctx, next) => {
  ctx.body = {
    code: 0,
    data: {
      name: '阿林十一'
    },
    msg: 'success'
  };
});

app.use(router.routes());  // 添加路由中間件
app.use(router.allowedMethods()); // 對請求進行一些限制處理

app.listen(9001);
複製代碼

使用 node server/index.js 啓動服務後,在 http://localhost:9001/user 能夠訪問結果。

以後再修改 handle.js,在點擊按鈕以後會請求接口

import './handle.css';

export function handleClick () {
  console.log('handleClick');

  fetch('/api/user')
    .then(r => r.json())
    .then(data => console.log(data))
    .catch(err => console.log(err));
}
複製代碼

這是會發現接口報 404,下面咱們配置一下 webpack.config.dev.js

// webpack.config.dev.js

module.exports = {
  //...
  proxy: {
    '/api': {
      target: 'http://127.0.0.1:9001/',
      pathRewrite: {
        '^/api': ''
      }
    }
  }
}
複製代碼

請求到 http://localhost:9000/api/user 如今會被代理到請求 http://localhost:9001/user。點擊按鈕發起請求:

最後

如今,咱們對 webpack 的配置有了更進一步的瞭解了,快動手試試吧。本文全部代碼能夠查看 github

後續將會繼續推出 webpack 系列的其餘內容哦~

喜歡本文的話點個贊吧~

更多精彩內容,歡迎關注微信公衆號~

相關文章
相關標籤/搜索