前言
下載啥的就不多說了,就看看我們專案中經常用到的一些配置。碼字不易,喜歡的話點個?哦 ~~~
webpack
webpack只是一個打包模組的機制,只是把依賴的模組轉化成可以代表這些包的靜態檔案。webpack就是識別你的 入口檔案。識別你的模組依賴,來打包你的程式碼。至於你的程式碼使用的是commonjs還是amd或者es6的import。webpack都會對其進行分析。來獲取程式碼的依賴。webpack做的就是分析程式碼。轉換程式碼,編譯程式碼,輸出程式碼。webpack本身是一個node的模組,所以webpack.config.js是以commonjs形式書寫的(node中的模組化是commonjs規範的) webpack中每個模組有一個唯一的id,是從0開始遞增的。整個打包後的bundle.js是一個匿名函式自執行。引數則為一個陣列。陣列的每一項都為個function。function的內容則為每個模組的內容,並按照require的順序排列。
clean-webpack-plugin
清空打包檔案。
new CleanWebpackPlugin()
, 可以不傳參,配置參考這裡
source map
sourcemap就是一個資訊檔案,裡面儲存著位置資訊。目的是為了解決開發程式碼與實際執行程式碼不一致時幫助我們debug到原始開發程式碼的技術。
- 配置 devtool 屬性 ( 最佳實踐 )
- development:
cheap-module-eval-source-map
- production:
cheap-module-source-map
- 五個關鍵字任意組合: eval,source-map,cheap,module,inline
- eval: 包裹模組,在模組尾新增模組來源 //#sourceURL,通過 sourceURL 找到原始程式碼位置,不單獨產生.map 檔案
- source-map: 產生.map檔案,包含原始程式碼和執行程式碼的對映關係
- 參考文獻
webpack-dev-server
npm i webpack-dev-server -D
- 注意:打包的檔案不會放到dist目錄中了,而是放在我們的記憶體中,從而提升了打包速度。
devServer: {
// open: true, // 開啟瀏覽器
// port: 8080, // default
hot: true, // HMR,不重新整理頁面就能應用你改過的css樣式
hotOnly: true, // 如果HMR沒生效,也不重新整理頁面
contentBase: './dist', // 告訴伺服器從哪裡提供內容。只有在您希望提供靜態檔案時才需要這樣做。
proxy: {
'/api': 'http://localhost:3000' // 如果我們訪問localhost:8000/api ,則轉發請求到localhost:3000
}
},
複製程式碼
- 通過配置hot: true 和 webpack.HotModuleReplacementPlugin() 可以及時更新css樣式而不重新整理頁面!
- 如果更變js程式碼,保證其它程式碼的狀態不發生變化,則需要另外加一段程式碼,如下:
+ if (module.hot) {
+ module.hot.accept('./print.js', function() {
+ // 檢測到print.js中的更改時,我們告訴webpack接受更新後的模組。
+ console.log('Accepting the updated printMe module!');
+ printMe();
+ })
+ }
複製程式碼
- 注意: 為什麼改變css程式碼不需要新增 module.hot.accpet 程式碼呢? 原因是因為 css-loader 已經幫我們處理過這一步了,js 程式碼就需要我們自己來新增 HMR 了。在使用 vue 的時候,vue-loader 幫我們實現了 js HMR 這一塊了,所以也不用我們自己實現了。react 則是藉助了 babel-preset 來幫我們實現了 js HMR.
webpack-dev-middleware
通過webpack-dev-middleware 配合 express 可以自己搭建一個簡單的webpack-dev-server,通過node來執行webpack,程式碼如下:
server.js
const webpack = require('webpack')
const middleware = require('webpack-dev-middleware')
const express = require('express')
const config = require('./webpack.config.js')
// webpack 編譯器
const compiler = webpack(config)
const app = express()
app.use(
middleware(compiler, {
// webpack-dev-middleware options
publicPath: config.output.publicPath
})
)
app.listen(3000, () => {
console.log('Example app listening on port 3000!')
})
複製程式碼
使用babel處理ES6
進入官網,開啟 setup, 進入webpack,檢視相關文件
// npm install --save-dev babel-loader @babel/core
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/, // 不包含
loader: "babel-loader" // webpack 和 babel 做通訊的橋樑
}
]
}
// 還需要配置options 或者 .babelrc 檔案
npm install @babel/preset-env --save-dev
{
test: /\.js$/,
exclude: /node_modules/, // 不包含
loader: "babel-loader", // webpack 和 babel 做通訊的橋樑
options: {
"presets": ["@babel/preset-env"]
}
}
// 但是,這只是將ES6 轉 ES5,有一些語法比如promiss,map等,低版本還是不認識,這就要使用 @babel/polyfill 了
複製程式碼
- @babel/polyfill
// 提供polyfill是為了方便,但是您應該將它與@babel/preset-env和useBuiltIns選項一起使用
// 這樣它就不會包含並非總是需要的整個polyfill。否則,我們建議您手動匯入各個填充。
npm install --save @babel/polyfill
// 然後在程式碼的頂部引入:
import "@babel/polyfill";
"presets": [["@babel/preset-env"], {
targets: {
chrome: "67", // chrome 版本大於67的,就不需要將ES6轉ES5了,因為chrome對ES6已經支援的很好了
"ie": "11"
},
useBuildIns: "usage" // useBuiltIns選項,如果設定成"usage",那麼將會自動檢測語法幫你require你程式碼中使用到的功能。也不需要額外引入@babel/polyfill 了
}]
複製程式碼
通過 .babelrc 來宣告 參考文件
- @babel/plugin-transform-runtime
- 避免多次編譯出helper函式:
- 這裡的 @babel/runtime 包就宣告瞭所有需要用到的幫助函式,而 @babel/plugin-transform-runtime 的作用就是將所有需要helper函式的檔案,依賴@babel/runtime包
- 解決@babel/polyfill提供的類或者例項方法汙染全域性作用域的情況。
// npm install --save-dev @babel/plugin-transform-runtime
// npm install --save @babel/runtime-corejs2
.babelrc
{
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 2, // 代表需要使用corejs的版本; npm install --save @babel/runtime-corejs2
"helpers": true,
"regenerator": true,
"useESModules": false // 按需引入babel/polyfill (注入低版本的polyfill)
}
]
]
}
複製程式碼
webpack打包React
安裝包 npm i @babel/preset-env @babel/preset-react -D
tree shaking
tree shaking 是一個術語,通常用於描述移除 JavaScript 上下文中的未引用程式碼(dead-code)。它依賴於 ES2015 模組語法的 靜態結構 特性,例如 import 和 export。不支援 require 的引入方式。這是因為 ES 模組的引入方式是靜態的,而require 的引入方式是動態的。
- 配置
- 將 mode 配置選項設定為 development 以確保 bundle 是未壓縮版本
mode: "development",
optimization: {
usedExports: true // 檢視那些模組被使用了,使用了的就打包
}
// 還要再 package.json 中配置一個屬性
"sideEffects": false
// "side effect(副作用)" 的定義是,在匯入時會執行特殊行為的程式碼,而不是僅僅暴露一個 export 或多個 export。舉例說明,例如 polyfill,它影響全域性作用域,並且通常不提供 export。
"sideEffects": ["@babel/polyfill", "*.css"] // 不對 @babel/polyfill和所有引入的css檔案 作tree shaking
複製程式碼
注意: 在開發環境下,使用tree shaking 其實還是會將沒有使用的模組打包進 bundle.js 中,只不過會提醒你那些模組沒有使用。如果使用 mode: production; 那我們就不需要使用 optimization 配置項了,並且將設定 devtool: cheap-module-source-map。但是 sideEffects 還是要使用。
區分打包(dev and prod)
npm i webpack-merge -D 用來合併 webpack 模組
- webpack.common.js,存放 dev 和 prod 公共的配置
- webpack.dev.js, 開發環境的配置
- webpack.prod.js,生產環境的配置
- 我們可以將以上的webpack配置檔案放入到 build 資料夾中統一管理
看看 webpack.prod.js 的用法
webpack.prod.js
const merge = require('webpack-merge')
const devConfig = require('webpack.common.js')
cosnt prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map'
}
module.exports = merge(prodConfig, merge)
複製程式碼
然後修改 package.json 中的配置
// 啟動dev
"dev": "webpack-dev-server --config ./build/webpack.dev.js"
// 啟動prod
"build": "webpack --config ./build/webpack.prod.js"
複製程式碼
注意: 如果我們的webpack配置放在build資料夾中,並且我們的配置中使用 clean-webpack-plugin,那麼它的配置也需要發生改變
new CleanWebpackPlugin(['dist']) // 指的是刪除當前目錄下的 dist 目錄,但是我們的 dist 目錄需要放在 build 資料夾同級目錄下。
// 可以這樣做 : 在 github 上搜尋 clean-webpack-plugin
new CleanWebpack(['dist'], {
root: path.resolve(__dirname, '../') // 指定根路徑
})
複製程式碼
Code Splitting (程式碼分割)
程式碼分離可以用於獲取更小的 bundle,以及控制資源載入優先順序,如果使用合理,會極大影響載入時間。
常用的程式碼分離方法有三種:
-
入口起點:使用 entry 配置手動地分離程式碼。(優點:最簡單最直觀。缺點:對我們的示例來說毫無疑問是個嚴重問題,因為我們在 ./src/index.js 中也引入過 lodash,這樣就造成在兩個 bundle 中重複引用。)
-
防止重複:使用 SplitChunksPlugin 去重和分離 chunk。
optimization: { // 這裡可以配置 code splitting, 還可以配置 tree shaking 時需要的 usedExports
splitChunks: {
chunks: 'all'
}
}
複製程式碼
將index.js 和 another_module.js 中的 lodash 庫抽離出來了。
- 動態匯入:
通過模組中的行內函數呼叫來分離程式碼。 我們不再使用 statically import(靜態匯入) lodash,而是通過 dynamic import(動態匯入) 來分離出一個 chunk。 檢視官網demo
環境變數
想要消除 開發環境 和 生產環境 之間的 webpack.config.js 差異,你可能需要環境變數(environment variable)。***參考文件***
- 配置
- 對於我們的 webpack 配置,有一個必須要修改之處。通常,module.exports 指向配置物件。要使用 env 變數,你必須將 module.exports 轉換成一個函式:
const path = require('path');
module.exports = env => {
// Use env.<YOUR VARIABLE> here:
console.log('NODE_ENV: ', env.NODE_ENV); // 'local'
console.log('Production: ', env.production); // true
return {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
};
複製程式碼
- 既然可以在配置檔案中接收到 env ,那麼我們就可以將上面的 mode 區分部分重新修改一下,怎麼修改呢?如下:
// 在上面的 環境區分中,我們在 webpack.common.js 中引入 devConfig、prodConfig、webpack-merge,然後通過 env 來判斷當前的打包命令是開發環境還是生產環境。
module.exports = env => {
if (env && env.production) {
return merge(commonConfig, prodConfig);
} else {
return merge(commonConfig, devConfig);
}
}
複製程式碼
Library 打包
除了打包應用程式,webpack 還可以用於打包 JavaScript library. 參考文件
- libraryTarget 還可以賦值為 'this' 或者 'window' | 'global',意味著將 library 掛載到 全域性上
- 如果我們自己寫的庫中引入的第三方庫,比如lodash.js,但是我們不希望它打包到我們的庫中,那麼應該怎麼辦呢?我們需要配置如下引數
// webpack.config.js
externals: ['lodash']
// 不打包lodash,但是別人引入我們的庫的時候,就必須引入lodash.js,因為我們的庫依賴lodash
複製程式碼
- 釋出自己的庫到 npm 上,給別人使用
- 首先配置我們的package.json檔案
- 到 npm 官網上註冊我們的賬號
- npm adduser (新增使用者名稱和密碼)
- npm publish (將我們自己的庫釋出到 npm 倉庫上去)
- 注意,庫的名字一定要特別,不能和npm 倉庫上的庫同名
漸進式網路應用程式 ( PWA )
我們通過搭建一個簡易 server 下,測試下這種離線體驗。這裡使用 http-server package:npm install http-server --save-dev 參考
{
"scripts": {
+ "build": "webpack",
+ "start": "http-server dist"
}
}
複製程式碼
- 新增 Workbox
npm install workbox-webpack-plugin --save-dev
const WorkboxPlugin = require('workbox-webpack-plugin');
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
+ title: '漸進式網路應用程式'
+ }),
+ new WorkboxPlugin.GenerateSW({
+ // 這些選項幫助快速啟用 ServiceWorkers
+ // 不允許遺留任何“舊的” ServiceWorkers
+ clientsClaim: true,
+ skipWaiting: true
+ })
]
複製程式碼
- 註冊 Service Worker
接下來我們註冊 Service Worker,使其出場並開始表演。通過新增以下注冊程式碼來完成此操作:
import _ from 'lodash';
import printMe from './print.js';
+ if ('serviceWorker' in navigator) {
+ window.addEventListener('load', () => {
+ navigator.serviceWorker.register('/service-worker.js').then(registration => {
+ console.log('SW registered: ', registration);
+ }).catch(registrationError => {
+ console.log('SW registration failed: ', registrationError);
+ });
+ });
+ }
複製程式碼
TypeScript
TypeScript 是 JavaScript 的超集,為其增加了型別系統,可以編譯為普通 JavaScript 程式碼 參考
npm install --save-dev typescript ts-loader
複製程式碼
- 在webpack.config.js中配置
- 需要建立一個tsconfig.json
{
"compilerOptions": {
"outDir": "./dist/", // 當前目錄下的dist
"noImplicitAny": true, //
"module": "es6", // 使用ES Module 引入方式
"target": "es5", // 轉換成 ES5 的程式碼
"jsx": "react",
"allowJs": true // 允許引入 js 檔案
}
}
複製程式碼
- 使用typescript時我們可能使用外部的庫,但是外部的庫是無法利用typescript自動檢測的特性的。這個時候我們就需要安裝這些庫的型別檔案,比如lodash, 我們如何安裝它的自動檢測型別檔案呢?
npm i @types/lodash -D // 通過 @types/ + 庫的名字即可
複製程式碼
devServer.proxy
// 請求到 /api/users 現在會被代理到請求 http://localhost:3000/api/users
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: {
'header.json': 'demo.json' //我們請求header.json的時候,轉而去請求demo.json
},
secure: false, // 預設情況下,不接受執行在 HTTPS 上,且使用了無效證照的後端伺服器。如果你想要接受,修改配置
bypass: function(req, res, proxyOptions) { // 攔截,如果請求的是 html 型別的資料,就直接返回 html檔案,不進行轉發
if (req.headers.accept.indexOf("html") !== -1) {
console.log("Skipping proxy for browser request.");
return "/index.html";
}
}
}
}
}
複製程式碼
devServer.historyApiFallback
當使用 HTML5 History API 時,任意的 404 響應都可能需要被替代為 index.html.在做單頁面路由的時候,需要配置此選項。
devServer: {
historyApiFallback: true
}
複製程式碼
ESlint配置
約束程式碼規範的工具,團隊開發尤其重要
// 安裝
npm i eslint -D
// 初始化配置檔案
npx eslint --init
複製程式碼
- 使用Airbnb公司的規範,我們需要另外覆蓋一些配置
- 在webpack中配置 eslint-loader
注意: 在webpack.devServer中配置一個屬性:npm i eslint-loader -D (會降低打包的速度哦 ! 可以在options中配置 cache 屬性) 參考官網配置
devServer: {
overlay: true, // 將報錯顯示在頁面上
}
複製程式碼
webpack效能提升
- 升級版本
- 比如webpack,node,npm,yarn
- 儘量少使用 Loader
- 在使用 babel-loader編譯 js 程式碼的時候,使用 exclude 或者 include來忽略掉 node_modules下的檔案,因為這個檔案下的檔案都是編譯過了的,就沒必要再次讓 babel-loader 來編譯了。
- 儘量少使用 Plugin
- 比如在開發環境中,我們就沒必要使用壓縮css或者js 程式碼了。
- 合理配置resolve
- resolve.alias: 建立 import 或 require 的別名,來確保模組引入變得更簡單
- resolve.extensions: 自動解析確定的擴充套件。預設值為:extensions: [".js", ".json"], 能夠使使用者在引入模組時不帶擴充套件:import File from '../path/to/file'.也可以新增 'jsx' 之類的檔案
- resolve.mainFields: 通過配置這個選項,來引入可以像省略 index.js 這樣的檔案。比如我們引入
import index from './src'
,其實就是引入的src下的index.js,我們也可以配置其它的檔案,比如 main.js,hello.js ...
- DllPlugin
DLLPlugin 和 DLLReferencePlugin 用某種方法實現了拆分 bundles,同時還大大提升了構建的速度。
- 實現的邏輯是,將第三方庫只打包一次並打包成一個檔案 (也可以打包成多個檔案,配置 entry 屬性就行了),然後將其快取起來,之後再做打包的時候,不再去node_modules中尋找了
- 建立一個 webpack.dll.js 配置檔案
將我們打包好的第三方庫原始碼引入到 HtmlWebpackPlugin 生成的 index.html 中
- 使用 webpack.DllRefrencePlugin 來分析程式碼
這個外掛是在 webpack 主配置檔案(webpack.common.js)中設定的。它會結合manifest.json、第三方庫原始碼檔案以及我們引入的第三方庫檔案做一個分析,如果它發現我們引入的檔案在原始碼檔案裡面已經有了,它就直接拿過來用了,而不會node_modules裡面找了。
new webpack.DllRefrencePlugin({
manifest: path.resolve(__dirname, './vendors.manifest.json')
})
複製程式碼
- 拆分成多個第三方庫檔案
- 控制包檔案大小
- 還記得 tree shaking 吧?去除冗餘程式碼 ...
- splitChunks 來將大檔案拆分成多個小檔案
- 多程式打包
webpack 預設是使用 node.js 來執行,即採用的單執行緒機制打包過程
- node裡面的多程式 thread-loader, parallel-webpack, happypack
- 合理使用 sourceMap
描述越詳細,就越慢哦 ~
- 結合 stats 分析打包結果
結合分析,檢視哪個模組打包耗時比較長,做針對性處理
- 開發環境記憶體編譯
採用 webpack-dev-server
- 開發環境剔除無用外掛
比如壓縮 css 或者 js 的外掛
- 這裡我們需要配置環境變數來區分打包、
多頁面打包配置
本質就是建立多個entry 以及 HtmlWebpackPlugin 來實現的
- 比如我們要打包兩個js檔案,並且通過兩個 index.html 分別引入
如何編寫 Loader
其實 loader 就是一些函式(不能使用箭頭函式哦,因為this),接受的引數就我們的原始碼,通過函式來做一些處理並返回而已。loaderAPI
// myself-loader.js
module.exports = function (source) {
return something ...
}
// 然後在 module 中配置
module: {
rules: [
{
test: /\.js$/,
loader: path.resolve(__dirname, './loaders/myself-loader.js')
}
]
}
// 如何傳參?
// 配置 options 之後,在我們寫的 laoder 裡面通過 this.query 就能獲取到啦 !!!
{
test: /\.js$/,
use: [
loader: path.resolve(__dirname, './loaders/myself-loader.js'),
options: {
name: 'alex.cheng'
}
]
}
// 自己定義的 loader 如何像引入 node_modules 裡面 loader 一樣引入呢 ?
resolveLoader: {
modules: ["node_modules", "./loader"] // 如果在node_modules裡沒找到,就到當前檔案loader中去找
}
複製程式碼
如何編寫一個Plugin
我們使用別人的 plugin 的時候都是怎麼使用的呢? 是不是都要 new Plugin ? 所以啊,我們的 plugin 都是通過建構函式來編寫的。來看一個簡單的例子
// plugin: alex-cheng-webpack-plugin.js
class AlexChengWebpackPlugin {
constructor() {
console.log('alex.cheng plugin is excuted !')
}
}
module.exports = AlexChengWebpackPlugin
// webpack.config.js
const path = require('path')
const AlexChengWebpackPlugin = require('./src/myPlugins/alex-cheng-webpack-plugin')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: '[name].js',
path: path.resolve(__dirname, './dist/')
},
plugins: [
new AlexChengWebpackPlugin() // 呼叫就行了啦 !!
]
}
複製程式碼
看!我們自己的外掛就執行了咯 ! 詳情可以參考 官網API