本文將繼續引入更多的 webpack
配置,建議先閱讀【webpack 系列】基礎篇的內容。若是發現文中有任何錯誤,請在評論區指正。本文全部代碼均可在 github 找到。css
以前咱們配置的是一個單頁的應用,可是咱們的應用可能須要是個多頁應用。下面咱們來進行多頁應用的 webpack
配置。 先看一下咱們的目錄結構html
├── public
│ ├── detail.html
│ └── index.html
├── src
│ ├── detail-entry.js
│ ├── index-entry.js
複製代碼
public
下面有 index.html
和 detail.html
兩個頁面,對應 src
下面有 index-entry.js
和 detail-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-plugin。git
webpack4
對 css
模塊支持的完善以及在處理 css
文件提取的方式上也作了些調整,由 mini-css-extract-plugin
來代替以前使用的 extract-text-webpack-plugin
,使用方式很簡單。es6
該插件將 css
提取到單獨的文件中,爲每一個包含 css
的 js
文件建立一個 css
文件,支持 css
和 sourcemap
的按需加載。 與 extract-text-webpack-plugin
相比有以下優勢github
css
安裝 extract-text-webpack-plugin
web
npm i -D mini-css-extract-plugin
複製代碼
配置 webpack.config.js
npm
// 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
文件了。
這時咱們發現兩個問題:
css
文件沒有進行壓縮。hash
部分都是同樣的,存在緩存問題。經過 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
文件進行壓縮了。
對於第二個問題,咱們首先須要瞭解下 hash
、chunkHash
、contentHash
的區別。
hash
是基於整個 module identifier
序列計算獲得的,webpack 默認爲給各個模塊分配一個 id
以做標識,用來處理模塊之間的依賴關係,默認的 id
命名規則是根據模塊引入的順序賦予一個整數(1
、2
、3
...)。任意修改、增長、刪除一個模塊的依賴,都會對整個 id
序列形成影響,從而改變 hash
值。也就是每次修改或者增刪任何一個文件,全部文件名的 hash
值都將改變,整個項目的文件緩存都將失效。
output: {
path: path.resolve(__dirname, 'dist'), // 輸出目錄
filename: '[name].[hash:6].js', // 輸出文件名
}
new MiniCssExtractPlugin({
filename: 'css/[name].[hash:6].css'
})
複製代碼
能夠看到打包後的 js
和 css
文件的 hash
值是同樣的,因此對於沒有發生改變的模塊而言,這樣作是不合理的。
固然能夠看到,對於圖片等資源該 hash
仍是能夠生成一個惟一值的。
chunkhash
根據不一樣的入口文件進行依賴文件解析、構建對應的 chunk
,生成對應的哈希值。咱們將 filename
配置成 chunkhash
來看一下打包的結果。
output: {
path: path.resolve(__dirname, 'dist'), // 輸出目錄
filename: '[name].[chunkhash:6].js', // 輸出文件名
}
new MiniCssExtractPlugin({
filename: 'css/[name].[chunkhash:6].css'
})
複製代碼
能夠看到此時打包以後的
index.js
和
detail.js
的
chunkhash
是不同的。可是會發現
index.js
和
index.css
以及
detail.js
和
detail.css
的
chunkhash
是一致的,而且任意改動
js
或者
css
都會引發對應的
css
和
js
文件的
chunkhash
的改變,這是不合理的。因此這裏抽離出來的
css
文件將使用
contenthash
,來區分
css
文件和
js
文件的更新。
contenthash
是針對文件內容級別的,只有你本身模塊的內容變了,那麼 hash
值才改變。
output: {
path: path.resolve(__dirname, 'dist'), // 輸出目錄
filename: '[name].[chunkhash:6].js', // 輸出文件名
}
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:6].css'
})
複製代碼
OK
,能夠看到分離出來的
css
文件已經和入口文件的
hash
值區分開了。
爲了實現理想的緩存,咱們通常這樣使用他們:
JS
文件使用 chunkhash
CSS
樣式文件使用 contenthash
gif|png|jpe?g|eot|woff|ttf|svg|pdf
等使用 hash
不少時候咱們並不須要在一個頁面中一次性加載全部的 js
或者 css
文件,而是應該是須要用到時纔去加載相應的 js
或者 css
文件。
好比,如今咱們須要點擊一個按鈕纔會使用對應的 js
、css
文件,須要 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
來定義生成的文件名
// 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
以後查看
handle
和
handle2
文件的
webpackChunkName
設置成同樣的,這樣這兩個文件將會打包在一塊兒生成一個文件,能夠減小請求數量。
開發過程當中,咱們但願在瀏覽器不刷新頁面的狀況下可以去加載咱們修改的代碼,來提升咱們的開發效率。咱們來看下如何配置:
webpack-dev-server
的熱更新開關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
這個變量來區分環境,這顯然不是一個很好的方式。 咱們最好可以須要區分一下開發環境和生產環境的配置文件。
咱們能夠給不一樣的環境定義不一樣的配置文件,可是這些文件將會有大量類似的配置,這時咱們能夠這樣來定義文件:
webpack.base.js
:定義公共的配置webpack.dev.js
:定義開發環境的配置webpack.prod.js
:定義生產環境的配置咱們能夠將一些公共的配置抽離到 webpack.base.js
,而後在 webpack.dev.js
和 webpack.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。
Webpack
在啓動後會從配置的入口模塊出發找出全部依賴的模塊,Resolve
配置 Webpack
如何尋找模塊所對應的文件。 Webpack
內置 JavaScript
模塊化語法解析功能,默認會採用模塊化標準里約定好的規則去尋找,但你也能夠根據本身的須要修改默認的規則。
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';
複製代碼
若是在導入文件時沒有帶後綴名,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
文件時能夠省略後綴名了。
resolve.modules
配置 webpack
去哪些目錄下尋找第三方模塊,默認是隻會去 node_modules
目錄下尋找。若是項目中某個文件夾下的模塊常常被導入,不但願寫很長的路徑,好比 import '../../../components/link'
,那麼就能夠經過配置 resolve.modules
來簡化。
// webpack.config.js
module.exports = {
//...
resolve: {
modules: ['./src/components', 'node_modules'] // 從左到右查找
}
}
複製代碼
這時,你就能夠經過 import 'link'
引入了。
有一些第三方模塊會針對不一樣環境提供幾份代碼。例如分別提供採用 es5
和 es6
的 2
份代碼,這 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']
複製代碼
resolve.enforceExtension
若是配置爲 true
,那麼全部導入語句都必需要帶文件後綴。
enforceModuleExtension
和 enforceExtension
做用相似,但 enforceModuleExtension
只對 node_modules
下的模塊生效。 由於安裝的第三方模塊中大多數導入語句沒帶文件後綴,若是這時你配置了 enforceExtension
爲 true
,那麼就須要配置 enforceModuleExtension: false
來兼容第三方模塊。
本地開發時,前端項目的端口號是 9000
,可是服務端多是 9001
,根據瀏覽器的同源策略,是不能直接請求到後端服務的。固然你能夠在後端配置 CORS
相關的頭部來實現跨域,其實也能夠經過 webpack
的配置來解決跨域問題。
首先,咱們起一個後端服務,安裝 koa
、koa-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
系列的其餘內容哦~
喜歡本文的話點個贊吧~
更多精彩內容,歡迎關注微信公衆號~