1. 多入口配置
首先我們來看下webpack入口怎么配置:
module.exports = {
entry: {
'index' : '../src/index.js'
}
}
多入口只需要配置多個key:value對即可,key就是你要發(fā)布的包名,主要為了后面的outpu準(zhǔn)備,value就是你入口文件所在的位置
那么問題來了,是不是我每次加一個入口就要手動去添加呢?這樣也太不智能了吧霞幅!
別急,不就是個讀取目錄嗎量瓜,glob可以辦到呀司恳,glob是一個node插件,可以幫助我們按照一定規(guī)則讀取文件的路徑绍傲,我們就可以根據(jù)一定的規(guī)則構(gòu)建出entry了扔傅。
var glob = requir('glob')
var getEntry = function() {
var entry = {}
var files = glob.sync(path.resolve(__dirname, '../src/*/index.js'))//你的入口文件相對于當(dāng)前的路徑
files.forEach(file => {
var key = file.split('/').splice(-2, 1)[0]
entry[key] = file
})
return entry
}
module.exports = {
entry: getEntry()
}
這樣就解決了我們的多入口問題了
2. 多出口配置
對于多出口webpack還是比較容易實現(xiàn)的,畢竟我們在entry下功夫拿到了key
{
output: {
filename: '[name]/js/vender.[hash].js',
path: path.resolve(__dirname, '../dist')
}
}
是不是很簡單,此時你已經(jīng)可以使用這套配置再加上常規(guī)的module配置開發(fā)多入口多出的js項目了铅鲤,但是距離真正的spa項目還差的遠(yuǎn)划提,因為你沒有HTML呀,也不能自動把打包好的js編譯到HTML文件中呀邢享,而且你要想本地調(diào)試鹏往,你得有個服務(wù)器吧,watch太麻煩得刷新頁面骇塘,你得有個HMR支持吧...是不是說到這已經(jīng)懵了伊履,別急別急后面還有更多要配置的-。-哈
3. 添加一個HTML模板
webpack有一個插件叫做html-webpack-plugin款违,這個插件可以幫助我們構(gòu)建出你想要的模板唐瀑。配置如下
{
plugins: [
new HtmlWebpackPlugin({
title: 'your title',
filename: 'index.html',// 模板要往哪發(fā)布
template: path.resolve(__dirname, '../dist/src/project1/index.html'), // 模板的存放位置
chunks: [name], // chunks主要用于多入口文件,也就是你引用哪些打包的文件
hash: true,
inject: true, // 默認(rèn)值,決定打包的js在html中的位置插爹,默認(rèn)在body底部
})
]
}
這樣我們就配置完了一個單入口的html-webpack-plugin哄辣。
那怎么實現(xiàn)多入口配置呢?答案是有幾個入口就new幾個HtmlWebpackPlugin赠尾,這個是無限制的力穗,但是我們手動去寫是不是太麻煩了,程序員嘛气嫁,一定要學(xué)會使用工具呀当窗!上面我們構(gòu)建entry的時候就得到了入口文件,那我們是不是也可以仿照上面的方法也構(gòu)建一個HtmlWebpackPlugin的實例數(shù)組呢寸宵?為了不浪費性能崖面,我們決定就在一個循環(huán)里搞定好了!嗯說干就干梯影,上代碼N自薄!
var getEntry = function() {
var entry = {}
var htmlPlugins = []
var files = glob.sync(path.resolve(__dirname, '../src/*/index.js'))//你的入口文件相對于當(dāng)前的路徑
files.forEach(file => {
var key = file.split('/').splice(-2, 1)[0]
entry[key] = file
htmlPlugins.push(new HtmlWebpackPlugin({
title: key,
filename: path.resolve(__dirname, '../dist/' + key + '/index.html'),
template: path.resolve(__dirname, '../src/' + key + '/index.html'),
chunk: [key],
hash: true,
inject: true
}))
})
return {entry, htmlPlugins}
}
var {htmlPlugins} = getEntry()
module.exports = {
plugins: [
...htmlPlugins
]
}
此時你激動地去運行了一下 webpack 確實包如期打好了光酣,但是我們發(fā)現(xiàn)里面的js文件貌似路徑不太對疏遏,恭喜,你又學(xué)到了一個新東西救军,就是publicPath,它是用來為你的資源指定正確的存放位置的倘零。
{
output: {
....
publicPath: '../', // 因為我們的html和js打包的路徑都是基于頁面應(yīng)用project1或者project2路徑打包的唱遭,所以需要跳出一級,引用路徑才能正常訪問到呈驶。
}
}
現(xiàn)在HTML拷泽,JS都具備了,我們就差一個本地服務(wù)就能玩起來了,別急webpack早就幫你安排好了server
4. webpack-dev-server配置
webpack-dev-server可以幫助我們在本地建立一個靜態(tài)資源服務(wù)器司致,并且為我們提供了HMR熱重載技術(shù)拆吆,使得我們不需要刷新就可以進(jìn)行組件更新和調(diào)試,像極了你在devtools中直接修改代碼的樣子脂矫!是不是很爽
devServer : {
contentBase: path.resolve(__dirname, '../dist'),// 指定了服務(wù)器根目錄
compress: true, // 啟用gzip壓縮
hot: true, // 啟用HMR
port: 8080,
publicPath: '/', // 如何訪問資源總是以'/'開頭,需要根據(jù)我們的output決定如何配置
}
此時運行 webpack serve枣耀,就可以在http://localhost:8080/project1/index.html訪問我們打包好的HTML應(yīng)用了。
5. 基礎(chǔ)module配置
此時我們具備了開發(fā)多入口多出口的工程化能力了庭再,那我們就可以開發(fā)react或者vue了捞奕,只要具有相應(yīng)的loader就好了,在webpack中拄轻,每一個文件就是module颅围,而loader其實就是一個函數(shù),可以流式地幫助我們處理文件恨搓。
module: {
rules: [
{
test: '/\.(js|jsx|ts|tsx)$/',
exclude: '/node_modules/',
loader: 'babel-loader'
},
{
test: '/\.vue$/',
loader: 'vue-loader'
},
{
test: '/\.(css | scss)$/',
use: [
'style-loader',
'css-loader',
'sass-loader'
]
}院促,
{
test: '/\.(jpg|png|jpeg|gif|eot|svg|ttf|woff|woff2)$/',
loader: 'url-loader'
}
]
},
plugins: [
new VueLoaderPlugin(),
]
好了,現(xiàn)在你真的可以去開發(fā)項目了8А一疯!但是配置還有很大的優(yōu)化空間
6. 打包優(yōu)化
-
抽離公共依賴
我們知道,我們一個頁面有時候可能會引用多個vue實例化root組件夺姑,但是基于現(xiàn)在的打包情況憎乙,我們只能把vue或者react打包到應(yīng)用程序中,整個包體積就會很大绰疤,那可不可以多個包公用一個依賴呢享完,比如我把依賴用CDN的方式引入可以嗎,當(dāng)然废膘,webpack在入口文件遞歸每一個module的時候竹海,就會組成一個構(gòu)建樹,構(gòu)建樹就叫做chunk丐黄,chunk給我們組織好了依賴關(guān)系斋配,所以我們也可以通知webpack,樹上的哪些東西我們不需要灌闺,webpack就會為我們進(jìn)行tree-shaking把這個依賴從樹上搖下來艰争。
通過配置externals就可以滿足需求
{ externals: { 'vue' : 'Vue', 'vue-router': 'VueRouter', // key就是你import進(jìn)來的包的位置,value就是暴露的包名 } }
然后我們在html模板中引入對應(yīng)的js即可桂对,值得注意的是引用時也可以找到性能最佳的引用甩卓,例如vue.runtime.min.js,要比我們的開發(fā)時構(gòu)建時esm體積小蕉斜,性能更好逾柿,因為去掉了模板編譯的代碼缀棍,體積小了30%左右,前提是你的vue文件已經(jīng)被vue-loader編譯為了render函數(shù)机错,才可以使用這個運行時爬范。
-
抽離CSS
現(xiàn)在我們的CSS是被style-loader直接打包到了項目js文件中,style-loader主要是幫助我們把css對象轉(zhuǎn)化成字符串并且通過DOM的api追加到head中的弱匪,大概原理是這樣青瀑,細(xì)節(jié)其實還是有很多考量的。
那我們?yōu)槭裁匆殡xCSS痢法,單純就是想把CSS拿出來狱窘?在JS中放著不也能正常顯示麼,費這勁干嘛财搁。這就要說到瀏覽器的解析機(jī)制了蘸炸,瀏覽器在得到HTML文檔的時候,從上到下HTML解析為DOM尖奔,然后生成DOM樹搭儒,同時解析CSS的時候生成CSSOM,生成CSSOM樹提茁,DOM樹和CSSOM樹結(jié)合構(gòu)建出了render樹淹禾,render樹確定好以后會根據(jù)dom和定位構(gòu)建出布局layout,最后進(jìn)行paint繪制茴扁,當(dāng)遇到內(nèi)聯(lián)的JS時铃岔,就會阻塞DOM的解析,而且當(dāng)上面的CSS沒有加載完畢的時候峭火,JavaScript 執(zhí)行將暫停毁习,直至 CSSOM 就緒(因為JS能查詢修改CSSOM和DOM)。
重新追加的內(nèi)嵌的CSS同理會讓CSSOM不得不重排或重繪卖丸,
所以我們找到了問題纺且,我們的JS打包時設(shè)置了output.inject = true,所以就會把代碼放到body后面稍浆,此時DOM已經(jīng)解析完了载碌,但是我們在JS代碼執(zhí)行的時候,又加了一大段CSS到head中衅枫,那瀏覽器不得不重新走一遍構(gòu)建流程嫁艇,不僅如此,用戶在沒有CSS的時候为鳄,看到的就是一塊幾乎沒有意義的首屏裳仆,CSS 是阻塞渲染的資源。需要將它盡早孤钦、盡快地下載到客戶端歧斟,以便縮短首次渲染的時間。所以我們要把CSS放在head中偏形,并且讓他盡快下載并加裝到瀏覽器參與構(gòu)建CSSOM與渲染静袖,而不是等到最后。
說了這么多題外話俊扭,我們看看如何抽離队橙,只需要在loader中新增一個loader處理,并且一定要把style-loader移除萨惑,因為你抽離了CSS為單獨文件捐康,就沒有document對象了,style-loader不僅沒有意義而且還會報錯庸蔼。
{
test: /.(sc|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader',
]
},
.......
plugins: [
new MiniCssExtractPlugin({filename: '[name]/css/[name].css'}) // path就是默認(rèn)的output.path
]
- 清除dist的冗余包
plugins: [
new CleanWebpackPlugin() // 默認(rèn)清除output.path
]
-
我們發(fā)現(xiàn)解总,開發(fā)的久了,項目里的入口文件越來越多姐仅,打包也變得非常慢花枫,有時候我們只想打包某一個指定目錄下的入口文件,并不想全部打包掏膏,有辦法操作嗎劳翰?
當(dāng)然,我們可以在node中讀取環(huán)境變量馒疹,從而根據(jù)命令行下發(fā)的指令去構(gòu)建entry佳簸,這樣就可以定點打包了。
具體流程大概是這樣:
命令行輸入打包目錄 => 通過環(huán)境變量取到目錄名=>根據(jù)目錄名在遍歷時過濾不需要的entry=>構(gòu)建出需要打包的entry=>正常打包即可
代碼實現(xiàn)也比較容易
1.命令行增加后綴: npm run dev --run:project1 2.process.env.npm_config_argv['original'] 獲取到命令值數(shù)組 3.截取得到project1颖变,具體怎么截取很簡單這里就不羅列了 4.寫入到環(huán)境變量生均,process.env.BUILD_DIR = 'project1' 5.let buildFile = process.env.BUILD_DIR files = files.filter(file => file.indexOf(buildFile)!==-1) 過濾一個新的文件集合 6.進(jìn)行構(gòu)建entry 7.正常打包
定義一些環(huán)境變量方便webpack打包時候注入環(huán)境變量到我們的js
plugins: [
new DefineWebpackPlugins({
'process.env.ENV_NAME' : "\"" + process.env.ENV_NAME + "\"",
'process.env.BASE_URL' : "\"" + process.env.BASE_URL + "\"""
})
]
- 把錯誤信息從控制臺或者命令行輸出到頁面
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
devServer: {
...
quiet: true,
overlay: {
errors: true
},
...
}
plugins: [
new FriendlyErrorsWebpackPlugin(),
]
- 寫一個webpack插件
- 一個 JavaScript 函數(shù)或 JavaScript 類,用于承接這個插件模塊的所有邏輯悼做;
- 在它原型上定義的
apply
方法疯特,會在安裝插件時被調(diào)用,并被 webpack compiler 調(diào)用一次肛走; - 指定一個觸及到 webpack 本身的事件鉤子漓雅,即下文會提及的 hooks,用于特定時機(jī)處理額外的邏輯朽色;
- 對 webpack 實例內(nèi)部做一些操作處理邻吞;
- 在功能流程完成后可以調(diào)用 webpack 提供的回調(diào)函數(shù);
插件就是一個函數(shù)葫男,函數(shù)的原型上要有一個apply方法抱冷,webpack會調(diào)用該方法,并且把webpack實例傳入apply中梢褐,webpack執(zhí)行會有一些事件鉤子供我們處理旺遮,并且我們可以對實例內(nèi)部進(jìn)行操作赵讯,還可以執(zhí)行回調(diào)。
根據(jù)插件所能觸及到的 event hook(事件鉤子)耿眉,對其進(jìn)行分類边翼。每個 event hook 都被預(yù)先定義為 synchronous hook(同步), asynchronous hook(異步), waterfall hook(瀑布), parallel hook(并行),而在 webpack 內(nèi)部會使用 call/callAsync 方法調(diào)用這些 hook鸣剪。 —— webpack 中文文檔
如果你不進(jìn)一步追究组底,那么按照如下所示的方式對不同鉤子進(jìn)行 tap 處理即可,其中 tap 方法用于同步處理筐骇,異步方式則可以調(diào)用 tapAsync 方法或 tapPromise 方法债鸡。
class HelloWorldPlugin {
apply(compiler) {
console.log(compiler, 'Hello World before!')
compiler.hooks.done.tap(
'HelloAsyncPlugin',
compilation => {
console.log("webpack build done.");
}
);
}
}
module.exports = HelloWorldPlugin
最后
webpack為我們提供了十分便利的模塊化開發(fā),推動了前端的組件化工程化铛纬,依靠流程式和豐富的拓展插件配置的打包模式厌均,在目前來說仍是被各大cli主流使用的打包工具,但是隨著ES MODULE的興起與落地饺鹃,基于snowpack的vite已經(jīng)面世莫秆,并且效率更高,速度更快悔详,且更為易用镊屎,有空我會專門出一期vite的新手配置教程供大家參考,最后放上webpack的全部配置與本地的架構(gòu)目錄茄螃。
架構(gòu)
build
plugins
env_config
index.js
webpack.base.conf.js
src
project1
index.js
index.html
project2
index.js
index.html
npm腳本命令
"scripts": {
"lint": "eslint --ext .js,.vue src",
"start:dev": "cross-env NODE_ENV=development webpack serve --config build/webpack.base.conf.js",
"watch": "npm run dev --m dev",
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "cross-env NODE_ENV=development webpack --config build/webpack.base.conf.js --progress"
},
環(huán)境變量配置
var env = {
dev: {
env_name: 'dev',
base_url: '//dev.webpack.com/'
},
test: {
env_name: 'test',
base_url: '//test.webpack.com/'
},
build: {
env_name: 'build',
base_url: '//build.webpack.com/'
}
}
let type = JSON.parse(process.env.npm_config_argv)['original'].pop()
let buildDir = ''
if(type.includes('--run:')) {
buildDir = type.replace('--run:', '')
}
if('dev_test_build'.includes(type)) {
type = type.split(':').pop()
} else {
type = 'dev'
}
console.log(type, 'type')
console.log(process.env.npm_config_argv, 'process.env.npm_config_argv')
process.env.ENV_NAME = env[type || 'dev'].env_name
process.env.BASE_URL = env[type || 'dev'].base_url
process.env.BUILD_DIR = buildDir
module.exports.env = env
webpack.base.conf.js
const path = require('path')
const glob = require('glob')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const {
VueLoaderPlugin
} = require('vue-loader')
const HelloWorldPlugin = require('./plugins/hell-world-plugin')
const vueLoaderConfig = require('./vue_config/vue-loader.conf')
require('./env_config/index.js')
function getEntry() {
let entrys = {}
let htmlPlugins = []
let files = glob.sync(path.resolve(__dirname, '../src/*/index.js'))
let buildFile = process.env.BUILD_DIR
files = files.filter(file => file.indexOf(buildFile)!==-1)
files.forEach(item => {
const name = item.split('/').splice(-2, 1)[0]
entrys[name] = item
htmlPlugins.push(new HtmlWebpackPlugin({
title: name,
filename: path.resolve(__dirname, '../dist/' + name + '/index.html'),
chunks: [name],
hash: true,
template: path.resolve(__dirname, '../src/' + name + '/index.html'),
inject: true
}))
})
return {entrys, htmlPlugins}
}
const {entrys, htmlPlugins} = getEntry()
module.exports = (env, option) => {
return {
entry: entrys,
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@' : path.resolve(__dirname, '../src/')
},
extensions: ['.js', '.jsx', '.vue']
},
output: {
filename: '[name]/js/vendor.[hash].js',
path: path.resolve(__dirname, '../dist'),
publicPath: '../'
},
devServer: {
contentBase: path.resolve(__dirname, '../dist'),
compress: true,
port: 8080,
hot: true,
quiet: true,
overlay: {
errors: true
},
publicPath: '/',
after() {
console.log(`服務(wù)已啟動---運行在 http://localhost:8080`)
}
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
// presets: ['@babel/preset-env', '@babel/preset-react', '@vue/babel-preset-jsx']
}
},
{
test: /.vue$/,
loader: 'vue-loader',
// options: vueLoaderConfig
},
{
test: /.(sc|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader',
]
},
{
test: /\.(jpg|png|jpeg|gif|eot|svg|ttf|woff|woff2)$/,
loader: "url-loader"
},
]
},
plugins: [
new CleanWebpackPlugin(),
new FriendlyErrorsWebpackPlugin(),
new VueLoaderPlugin(),
new webpack.DefinePlugin({
'process.env.ENV_NAME' : "\"" + process.env.ENV_NAME + "\"",
'process.env.BASE_URL' : "\"" + process.env.BASE_URL + "\""
}),
new HelloWorldPlugin({options: true}),
new MiniCssExtractPlugin({filename: '[name]/css/[name].css'}),
...htmlPlugins
],
externals: {
'vue': 'Vue',
'vue-router': "VueRouter"
}
}
}