若是你尚未閱讀《4W字長文帶你深度解鎖Webpack系列(基礎篇)》,建議閱讀以後,再繼續閱讀本篇文章。css
本文會引入更多的 webpack
配置,若是文中有任何錯誤,歡迎在評論區指正,我會盡快修正。 webpack
優化部分放在了下一篇。html
推薦你們參考本文一步一步進行配置,不要老是想着找什麼最佳配置,你掌握了以後,根據本身的需求配置出來的,就是最佳配置。前端
本文對應的項目地址(編寫本文時使用) 供參考:https://github.com/YvetteLau/...vue
有些時候,咱們須要使用已有的JS文件、CSS文件(本地文件),可是不須要 webpack
編譯。例如,咱們在 public/index.html
中引入了 public
目錄下的 js
或 css
文件。這個時候,若是直接打包,那麼在構建出來以後,確定是找不到對應的 js
/ css
了。node
public
目錄結構
├── public │ ├── config.js │ ├── index.html │ ├── js │ │ ├── base.js │ │ └── other.js │ └── login.html
如今,咱們在 index.html
中引入了 ./js/base.js
。react
<!-- index.html --> <script src="./js/base.js"></script>
這時候,咱們 npm run dev
,會發現有找不到該資源文件的報錯信息。jquery
對於這個問題,咱們能夠手動將其拷貝至構建目錄,而後在配置 CleanWebpackPlugin
時,注意不要清空對應的文件或文件夾便可,可是如若這個靜態文件時不時的還會修改下,那麼依賴於手動拷貝,是很容易出問題的。webpack
不要過於相信本身的記性,依賴於手動拷貝的方式,大多數人應該都有過忘記拷貝的經歷,你要是說你歷來沒忘過。git
幸運的是,webpack
爲咱們這些記性很差又愛偷懶的人提供了好用的插件 CopyWebpackPlugin,它的做用就是將單個文件或整個目錄複製到構建目錄。github
首先安裝一下依賴:
npm install copy-webpack-plugin -D
修改配置(當前,須要作的是將 public/js
目錄拷貝至 dist/js
目錄):
//webpack.config.js const CopyWebpackPlugin = require('copy-webpack-plugin'); module.exports = { //... plugins: [ new CopyWebpackPlugin([ { from: 'public/js/*.js', to: path.resolve(__dirname, 'dist', 'js'), flatten: true, }, //還能夠繼續配置其它要拷貝的文件 ]) ] }
此時,從新執行 npm run dev
,報錯信息已經消失。
這裏說一下 flatten
這個參數,設置爲 true
,那麼它只會拷貝文件,而不會把文件夾路徑都拷貝上,你們能夠不設置 flatten
時,看下構建結果。
另外,若是咱們要拷貝一個目錄下的不少文件,可是想過濾掉某個或某些文件,那麼 CopyWebpackPlugin
還爲咱們提供了 ignore
參數。
//webpack.config.js const CopyWebpackPlugin = require('copy-webpack-plugin'); module.exports = { //... plugins: [ new CopyWebpackPlugin([ { from: 'public/js/*.js', to: path.resolve(__dirname, 'dist', 'js'), flatten: true, } ], { ignore: ['other.js'] }) ] }
例如,這裏咱們忽略掉 js
目錄下的 other.js
文件,使用 npm run build
構建,能夠看到 dist/js
下不會出現 other.js
文件。 CopyWebpackPlugin
還提供了不少其它的參數,若是當前的配置不能知足你,能夠查閱文檔進一步修改配置。
ProvidePlugin
在我看來,是爲懶人準備的,不過也別過分使用,畢竟全局變量不是什麼「好東西」。ProvidePlugin
的做用就是不須要 import
或 require
就能夠在項目中處處使用。
ProvidePlugin
是 webpack
的內置插件,使用方式以下:
new webpack.ProvidePlugin({ identifier1: 'module1', identifier2: ['module2', 'property2'] });
默認尋找路徑是當前文件夾 ./**
和 node_modules
,固然啦,你能夠指定全路徑。
React
你們都知道的,使用的時候,要在每一個文件中引入 React
,否則馬上拋錯給你看。還有就是 jquery
, lodash
這樣的庫,可能在多個文件中使用,可是懶得每次都引入,好嘛,一塊兒來偷個懶,修改下 webpack
的配置:
const webpack = require('webpack'); module.exports = { //... plugins: [ new webpack.ProvidePlugin({ React: 'react', Component: ['react', 'Component'], Vue: ['vue/dist/vue.esm.js', 'default'], $: 'jquery', _map: ['lodash', 'map'] }) ] }
這樣配置以後,你就能夠在項目中爲所欲爲的使用 $
、_map
了,而且寫 React
組件時,也不須要 import
React
和 Component
了,若是你想的話,你還能夠把 React
的 Hooks
都配置在這裏。
另外呢,Vue
的配置後面多了一個 default
,這是由於 vue.esm.js
中使用的是 export default
導出的,對於這種,必需要指定 default
。React
使用的是 module.exports
導出的,所以不要寫 default
。
另外,就是若是你項目啓動了 eslint
的話,記得修改下 eslint
的配置文件,增長如下配置:
{ "globals": { "React": true, "Vue": true, //.... } }
固然啦,偷懶要有個度,你要是配一大堆全局變量,最終可能會給本身帶來麻煩,對本身配置的全局變量必定要負責到底。
CSS打包咱們前面已經說過了,不過呢,有些時候,咱們可能會有抽離CSS的需求,即將CSS文件單獨打包,這多是由於打包成一個JS文件太大,影響加載速度,也有多是爲了緩存(例如,只有JS部分有改動),還有可能就是「我高興」:我想抽離就抽離,誰也管不着。
無論你是由於什麼緣由要抽離CSS,只要你有需求,咱們就能夠去實現。
首先,安裝 loader
:
npm install mini-css-extract-plugin -D
mini-css-extract-plugin
和extract-text-webpack-plugin
相比:
修改咱們的配置文件:
//webpack.config.js const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { plugins: [ new MiniCssExtractPlugin({ filename: 'css/[name].css' //我的習慣將css文件放在單獨目錄下 }) ], module: { rules: [ { test: /\.(le|c)ss$/, use: [ MiniCssExtractPlugin.loader, //替換以前的 style-loader 'css-loader', { loader: 'postcss-loader', options: { plugins: function () { return [ require('autoprefixer')({ "overrideBrowserslist": [ "defaults" ] }) ] } } }, 'less-loader' ], exclude: /node_modules/ } ] } }
如今,咱們從新編譯:npm run build
,目錄結構以下所示:
. ├── dist │ ├── assets │ │ ├── alita_e09b5c.jpg │ │ └── thor_e09b5c.jpeg │ ├── css │ │ ├── index.css │ │ └── index.css.map │ ├── bundle.fb6d0c.js │ ├── bundle.fb6d0c.js.map │ └── index.html
前面說了最好新建一個 .browserslistrc
文件,這樣能夠多個 loader
共享配置,因此,動手在根目錄下新建文件 (.browserslistrc
),內容以下(你能夠根據本身項目需求,修改成其它的配置):
last 2 version > 0.25% not dead
修改 webpack.config.js
:
//webpack.config.js const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { //... plugins: [ new MiniCssExtractPlugin({ filename: 'css/[name].css' }) ], module: { rules: [ { test: /\.(c|le)ss$/, use: [ MiniCssExtractPlugin.loader, 'css-loader', { loader: 'postcss-loader', options: { plugins: function () { return [ require('autoprefixer')() ] } } }, 'less-loader' ], exclude: /node_modules/ }, ] } }
要測試本身的 .browserlistrc
有沒有生效也很簡單,直接將文件內容修改成 last 1 Chrome versions
,而後對比修改先後的構建出的結果,就能看出來啦。
能夠查看更多[browserslistrc]配置項(https://github.com/browsersli...
更多配置項,能夠查看mini-css-extract-plugin
使用 mini-css-extract-plugin
,CSS
文件默認不會被壓縮,若是想要壓縮,須要配置 optimization
,首先安裝 optimize-css-assets-webpack-plugin
.
npm install optimize-css-assets-webpack-plugin -D
修改webpack配置:
//webpack.config.js const OptimizeCssPlugin = require('optimize-css-assets-webpack-plugin'); module.exports = { entry: './src/index.js', //.... plugins: [ new OptimizeCssPlugin() ], }
注意,這裏將 OptimizeCssPlugin
直接配置在 plugins
裏面,那麼 js
和 css
都可以正常壓縮,若是你將這個配置在 optimization
,那麼須要再配置一下 js
的壓縮(開發環境下不須要去作CSS的壓縮,所以後面記得將其放到 webpack.config.prod.js
中哈)。
配置完以後,測試的時候發現,抽離以後,修改 css
文件時,第一次頁面會刷新,可是第二次頁面不會刷新 —— 好嘛,我平時的業務中用不着抽離 css
,這個問題擱置了好多天(準確來講是忘記了)。
昨晚(0308)再次修改這篇文章的時候,正好看到了 MiniCssExtractPlugin.loader
對應的 option
設置,咱們再次修改下對應的 rule
。
module.exports = { rules: [ { test: /\.(c|le)ss$/, use: [ { loader: MiniCssExtractPlugin.loader, options: { hmr: isDev, reloadAll: true, } }, //... ], exclude: /node_modules/ } ] }
不少時候咱們不須要一次性加載全部的JS文件,而應該在不一樣階段去加載所須要的代碼。webpack
內置了強大的分割代碼的功能能夠實現按需加載。
好比,咱們在點擊了某個按鈕以後,才須要使用使用對應的JS文件中的代碼,須要使用 import()
語法:
document.getElementById('btn').onclick = function() { import('./handle').then(fn => fn.default()); }
import()
語法,須要 @babel/plugin-syntax-dynamic-import
的插件支持,可是由於當前 @babel/preset-env
預設中已經包含了 @babel/plugin-syntax-dynamic-import
,所以咱們不須要再單獨安裝和配置。
直接 npm run build
進行構建,構建結果以下:
webpack
遇到 import(****)
這樣的語法的時候,會這樣處理:
Chunk
import
所在的語句時,纔會加載該 Chunk
所對應的文件(如這裏的1.bundle.8bf4dc.js)你們能夠在瀏覽器中的控制檯中,在 Network
的 Tab頁
查看文件加載的狀況,只有點擊以後,纔會加載對應的 JS
。
devServer
的 hot
爲 true
plugins
中增長 new webpack.HotModuleReplacementPlugin()
//webpack.config.js const webpack = require('webpack'); module.exports = { //.... devServer: { hot: true }, plugins: [ new webpack.HotModuleReplacementPlugin() //熱更新插件 ] }
咱們配置了 HotModuleReplacementPlugin
以後,會發現,此時咱們修改代碼,仍然是整個頁面都會刷新。不但願整個頁面都刷新,還須要修改入口文件:
if(module && module.hot) { module.hot.accept() }
此時,再修改代碼,不會形成整個頁面的刷新。
有時,咱們的應用不必定是一個單頁應用,而是一個多頁應用,那麼如何使用 webpack
進行打包呢。爲了生成目錄看起來清晰,不生成單獨的 map
文件。
//webpack.config.js const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: { index: './src/index.js', login: './src/login.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[hash:6].js' }, //... plugins: [ new HtmlWebpackPlugin({ template: './public/index.html', filename: 'index.html' //打包後的文件名 }), new HtmlWebpackPlugin({ template: './public/login.html', filename: 'login.html' //打包後的文件名 }), ] }
若是須要配置多個 HtmlWebpackPlugin
,那麼 filename
字段不可缺省,不然默認生成的都是 index.html
,若是你但願 html
的文件名中也帶有 hash
,那麼直接修改 fliename
字段便可,例如: filename: 'login.[hash:6].html'
。
生成目錄以下:
. ├── dist │ ├── 2.463ccf.js │ ├── assets │ │ └── thor_e09b5c.jpeg │ ├── css │ │ ├── index.css │ │ └── login.css │ ├── index.463ccf.js │ ├── index.html │ ├── js │ │ └── base.js │ ├── login.463ccf.js │ └── login.html
看起來,彷佛是OK了,不過呢,查看 index.html
和 login.html
會發現,都同時引入了 index.f7d21a.js
和 login.f7d21a.js
,一般這不是咱們想要的,咱們但願,index.html
中只引入 index.f7d21a.js
,login.html
只引入 login.f7d21a.js
。
HtmlWebpackPlugin
提供了一個 chunks
的參數,能夠接受一個數組,配置此參數僅會將數組中指定的js引入到html文件中,此外,若是你須要引入多個JS文件,僅有少數不想引入,還能夠指定 excludeChunks
參數,它接受一個數組。
//webpack.config.js module.exports = { //... plugins: [ new HtmlWebpackPlugin({ template: './public/index.html', filename: 'index.html', //打包後的文件名 chunks: ['index'] }), new HtmlWebpackPlugin({ template: './public/login.html', filename: 'login.html', //打包後的文件名 chunks: ['login'] }), ] }
執行 npm run build
,能夠看到 index.html
中僅引入了 index
的 JS
文件,而 login.html
中也僅引入了 login
的 JS
文件,符合咱們的預期。
resolve
配置 webpack
如何尋找模塊所對應的文件。webpack
內置 JavaScript
模塊化語法解析功能,默認會採用模塊化標準里約定好的規則去尋找,但你能夠根據本身的須要修改默認的規則。
resolve.modules
配置 webpack
去哪些目錄下尋找第三方模塊,默認狀況下,只會去 node_modules
下尋找,若是你咱們項目中某個文件夾下的模塊常常被導入,不但願寫很長的路徑,那麼就能夠經過配置 resolve.modules
來簡化。
//webpack.config.js module.exports = { //.... resolve: { modules: ['./src/components', 'node_modules'] //從左到右依次查找 } }
這樣配置以後,咱們 import Dialog from 'dialog'
,會去尋找 ./src/components/dialog
,再也不須要使用相對路徑導入。若是在 ./src/components
下找不到的話,就會到 node_modules
下尋找。
resolve.alias
配置項經過別名把原導入路徑映射成一個新的導入路徑,例如:
//webpack.config.js module.exports = { //.... resolve: { alias: { 'react-native': '@my/react-native-web' //這個包名是我隨便寫的哈 } } }
例如,咱們有一個依賴 @my/react-native-web
能夠實現 react-native
轉 web
。咱們代碼通常下面這樣:
import { View, ListView, StyleSheet, Animated } from 'react-native';
配置了別名以後,在轉 web 時,會從 @my/react-native-web
尋找對應的依賴。
固然啦,若是某個依賴的名字太長了,你也能夠給它配置一個短一點的別名,這樣用起來比較爽,尤爲是帶有 scope
的包。
適配多端的項目中,可能會出現 .web.js
, .wx.js
,例如在轉web的項目中,咱們但願首先找 .web.js
,若是沒有,再找 .js
。咱們能夠這樣配置:
//webpack.config.js module.exports = { //.... resolve: { extensions: ['web.js', '.js'] //固然,你還能夠配置 .json, .css } }
首先尋找 ../dialog.web.js
,若是不存在的話,再尋找 ../dialog.js
。這在適配多端的代碼中很是有用,不然,你就須要根據不一樣的平臺去引入文件(以犧牲了速度爲代價)。
import dialog from '../dialog';
固然,配置 extensions
,咱們就能夠缺省文件後綴,在導入語句沒帶文件後綴時,會自動帶上extensions
中配置的後綴後,去嘗試訪問文件是否存在,所以要將高頻的後綴放在前面,而且數組不要太長,減小嚐試次數。若是沒有配置 extensions
,默認只會找對對應的js文件。
若是配置了 resolve.enforceExtension
爲 true
,那麼導入語句不能缺省文件後綴。
有一些第三方模塊會提供多份代碼,例如 bootstrap
,能夠查看 bootstrap
的 package.json
文件:
{ "style": "dist/css/bootstrap.css", "sass": "scss/bootstrap.scss", "main": "dist/js/bootstrap", }
resolve.mainFields
默認配置是 ['browser', 'main']
,即首先找對應依賴 package.json
中的 brower
字段,若是沒有,找 main
字段。
如:import 'bootstrap'
默認狀況下,找得是對應的依賴的 package.json
的 main
字段指定的文件,即 dist/js/bootstrap
。
假設咱們但願,import 'bootsrap'
默認去找 css
文件的話,能夠配置 resolve.mainFields
爲:
//webpack.config.js module.exports = { //.... resolve: { mainFields: ['style', 'main'] } }
目前爲止咱們 webpack
的配置,都定義在了 webpack.config.js
中,對於須要區分是開發環境仍是生產環境的狀況,咱們根據 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 install webpack-merge -D
const merge = require('webpack-merge'); merge({ devtool: 'cheap-module-eval-source-map', module: { rules: [ {a: 1} ] }, plugins: [1,2,3] }, { devtool: 'none', mode: "production", module: { rules: [ {a: 2}, {b: 1} ] }, plugins: [4,5,6], }); //合併後的結果爲 { devtool: 'none', mode: "production", module: { rules: [ {a: 1}, {a: 2}, {b: 1} ] }, plugins: [1,2,3,4,5,6] }
webpack.config.base.js
中是通用的 webpack
配置,以 webpack.config.dev.js
爲例,以下:
//webpack.config.dev.js const merge = require('webpack-merge'); const baseWebpackConfig = require('./webpack.config.base'); module.exports = merge(baseWebpackConfig, { mode: 'development' //...其它的一些配置 });
而後修改咱們的 package.json
,指定對應的 config
文件:
//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.prod.js" }, }
你可使用 merge
合併,也可使用 merge.smart
合併,merge.smart
在合併loader
時,會將同一匹配規則的進行合併,webpack-merge
的說明文檔中給出了詳細的示例。
不少時候,咱們在開發環境中會使用預發環境或者是本地的域名,生產環境中使用線上域名,咱們能夠在 webpack
定義環境變量,而後在代碼中使用。
使用 webpack
內置插件 DefinePlugin
來定義環境變量。
DefinePlugin
中的每一個鍵,是一個標識符.
value
是一個字符串,會被當作 code
片斷value
不是一個字符串,會被stringify
value
是一個對象,正常對象定義便可key
中有 typeof
,它只針對 typeof
調用定義//webpack.config.dev.js const webpack = require('webpack'); module.exports = { plugins: [ new webpack.DefinePlugin({ DEV: JSON.stringify('dev'), //字符串 FLAG: 'true' //FLAG 是個布爾類型 }) ] }
//index.js if(DEV === 'dev') { //開發環境 }else { //生產環境 }
假設前端在3000端口,服務端在4000端口,咱們經過 webpack
配置的方式去實現跨域。
首先,咱們在本地建立一個 server.js
:
let express = require('express'); let app = express(); app.get('/api/user', (req, res) => { res.json({name: '劉小夕'}); }); app.listen(4000);
執行代碼(run code
),如今咱們能夠在瀏覽器中訪問到此接口: http://localhost:4000/api/user
。
在 index.js
中請求 /api/user
,修改 index.js
以下:
//須要將 localhost:3000 轉發到 localhost:4000(服務端) 端口 fetch("/api/user") .then(response => response.json()) .then(data => console.log(data)) .catch(err => console.log(err));
咱們但願經過配置代理的方式,去訪問 4000 的接口。
修改 webpack
配置:
//webpack.config.js module.exports = { //... devServer: { proxy: { "/api": "http://localhost:4000" } } }
從新執行 npm run dev
,能夠看到控制檯打印出來了 {name: "劉小夕"}
,實現了跨域。
大多狀況,後端提供的接口並不包含 /api
,即:/user
,/info
、/list
等,配置代理時,咱們不可能羅列出每個api。
修改咱們的服務端代碼,並從新執行。
//server.js let express = require('express'); let app = express(); app.get('/user', (req, res) => { res.json({name: '劉小夕'}); }); app.listen(4000);
儘管後端的接口並不包含 /api
,咱們在請求後端接口時,仍然以 /api
開頭,在配置代理時,去掉 /api
,修改配置:
//webpack.config.js module.exports = { //... devServer: { proxy: { '/api': { target: 'http://localhost:4000', pathRewrite: { '/api': '' } } } } }
從新執行 npm run dev
,在瀏覽器中訪問: http://localhost:3000/
,控制檯中也打印出了{name: "劉小夕"}
,跨域成功,
簡單數據模擬
module.exports = { devServer: { before(app) { app.get('/user', (req, res) => { res.json({name: '劉小夕'}) }) } } }
在 src/index.js
中直接請求 /user
接口。
fetch("user") .then(response => response.json()) .then(data => console.log(data)) .catch(err => console.log(err));
使用 mocker-api mock數據接口
mocker-api 爲 REST API 建立模擬 API。在沒有實際 REST API 服務器的狀況下測試應用程序時,它會頗有用。
npm install mocker-api -D
module.exports = { 'GET /user': {name: '劉小夕'}, 'POST /login/account': (req, res) => { const { password, username } = req.body if (password === '888888' && username === 'admin') { return res.send({ status: 'ok', code: 0, token: 'sdfsdfsdfdsf', data: { id: 1, name: '劉小夕' } }) } else { return res.send({ status: 'error', code: 403 }) } } }
webpack.config.base.js
:const apiMocker = require('mocker-api'); module.export = { //... devServer: { before(app){ apiMocker(app, path.resolve('./mock/mocker.js')) } } }
這樣,咱們就能夠直接在代碼中像請求後端接口同樣對mock數據進行請求。
npm run dev
,能夠看到,控制檯成功打印出來 {name: '劉小夕'}
src/index.js
,檢查下POST接口是否成功//src/index.js fetch("/login/account", { method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ username: "admin", password: "888888" }) }) .then(response => response.json()) .then(data => console.log(data)) .catch(err => console.log(err));
能夠在控制檯中看到接口返回的成功的數據。
進階篇就到這裏結束啦,下週約優化篇。
關注公衆號
參考: