導(dǎo)航
前置知識
wepback打包優(yōu)化 ( 注意第4點(diǎn)splitChunks和dllPlugin的區(qū)別 )
手寫一個loader
(1) 前置知識
一. 從零開始配置webpack可能會用到的依賴
- webpack webpack-cli
- html-weback-plugin
- 優(yōu)化項(xiàng):minify:{ collapseWhitespace, removeAttributeQuotes} 折疊成一行,刪除html屬性的引號
- 注意:優(yōu)化項(xiàng)minify主要用于mode: 'production'因?yàn)閴嚎s會增加打包時間
- collapse 是坍塌的意思
- hash: true // html文件引入資源時,在資源后面添加hash串
- template
- filename
- webpack-dev-server
- contentBase // 啟動服務(wù)的文件夾
- open: true //打開瀏覽器
- port
- compress: true // 開啟gzip壓縮
- progress: true //打包進(jìn)程
- hot: true //開啟熱更新,主要用于 webpack.HotModuleReplacementPlugin()
- proxy // 設(shè)置代理
- before // 鉤子函數(shù),用來模擬數(shù)據(jù)
- webpack-dev-server除了在配置文件中配置,還可以寫成命令行,如下
- webpack-dev-server --open --compress --progress --color
- css相關(guān)
- style-loader
- css-loader
- less-loader
- 抽離css
- mini-css-extract-plugin 抽離css
- MiniCssExtractPlugin.loader代替style-loader,因?yàn)椴挥胹tyle方式插入,而是抽離單獨(dú)文件
- 在plugins中 new MiniCssExtractPlugin({ filename, chunkFilname, ignoreOrder })
- 前綴
- postcss-loader 解決css前綴,需要另外配置postcss.config.js,注意順序,放在css-loader下面,先加載
- 單獨(dú)配置 postcss.config.js
- 需要配合 autoprefixer
- 注意:autoprefixer需要給出瀏覽器的一些信息,所以要在 package.json 中添加 browserslist
- autoprefixer 解決css前綴,還需要package.json中配置 browserslist
- "browserslist": [ // 在package.json中添加
"defaults",
"not ie < 9",
"last 3 version",
">1%",
"iOS 7",
"last 3 iOS versions"
]
- 壓縮css
- (注意用于生產(chǎn)環(huán)境才去壓縮css,因?yàn)橛绊懘虬俣?
- optimize-css-assets-webapck-plugin 優(yōu)化css,比如壓縮,壓縮完后,production環(huán)境還需使用uglifyjs壓縮js
- uglifyjs-webpack-plugin 壓縮js
- optimization: { // 需要把 mode: 'production’才能看到壓縮css和js的效果,和上面的html的優(yōu)化一樣
minimizer: [
new OptimizeCssAssetsWebpackPlugin({}),
new UglifyjsWebpackPlugin({
cache: true,
parallel: true, // 平行,并行的意思
sourceMap: true, // 調(diào)試映射
})
]
}
- css: 如何把所要打包的css抽離到css/目錄下 => MiniCssExtractPlugin => filename => 'css/[name].css'
- https://webpack.js.org/plugins/mini-css-extract-plugin/#root
- js相關(guān)
- babel-loader
- @babel/core
- @babel/preset-env
- @babel/plugin-proposal-decorators 注意順序和 legacy 配置項(xiàng) // legacy: 是遺產(chǎn)的意思
- @babel/plugin-proposal-class-properties 注意loose配置項(xiàng)
- @babel/plugin-transform-runtime
- @babel/runtime 注意是 dependencies 而不是 devDependencies
- @babel/polyfill 注意是 dependencies 不是 devDependencies,還需在入口js文件引入,才能解析更高級的js語法,如includes
- .babelrc
-
{
"presets": [
["@babel/preset-env", {
"modules": false // 關(guān)閉babel的模塊轉(zhuǎn)換功能,保留 es6 模塊化語法
}]
],
"plugins": [
["@babel/plugin-proposal-decorators", {"legacy": true}], // 注意順序,需要寫在class插件的前面,legacy不能少
["@babel/plugin-proposal-class-properties", {"loose": true}], // proposal:是提案的意思
["@babel/plugin-transform-runtime"], // 配置項(xiàng)都可以寫成數(shù)組形式,后面還可根一個配置對象
["@babel/plugin-syntax-dynamic-import"] // 用于來加載模塊,語法動態(tài)加載插件
]
}
- eslint eslint-loader babel-eslint
- 需要單獨(dú)配置 .eslintrc.json 文件,配置rules
- eslint-loader如果需要保證加載的順序最先執(zhí)行,要設(shè)置配置項(xiàng): enforce: 'pre'
- babel-eslint // 需要安裝bebel-eslint插件 Cannot find module 'babel-eslint'
{
"parser": "babel-eslint", // 這里必須設(shè)置
"parserOptions": {
"sourceType": "module",
"allowImportExportEverywhere": true
},
"rules": {
"indent": "off"
},
"env": {}
}
- loader的類型
- pre 前置loader
- post 后置loader
- normal 普通loader
- inline 內(nèi)聯(lián)loader
- expose-loader
- expose-loader用來暴露全局對象,即可以通過window.屬性的方式訪問
- expose: 是暴露的意思
- 書寫方式有三種
- (1)
- 直接在入口js中引入,require('expose-loader?$!jquery') // 把jquery用變量 $ 暴露給全局
- (2)
- 在webpack.config.js的module中配置expose-loader
- {
test: require.resolve('jquery'), // require.resolve()是nodejs中的函數(shù)
use: [
{
loader: 'expose-loader',
options: 'jquery' // 暴露成window.jquery
},
{
loader: 'expose-loader',
options: '$' // 暴露成window.$
}
]
},
- (3)
- 在每個模塊中注入$,不需要在每個模塊中再引入,可以使用 webpack.ProvidePlugin插件,再plugins中加入
- new webpack.ProvidePlugin({
$: 'jquery'
})
- 圖片相關(guān)
- file-loader
- url-loader 可以設(shè)置大小限制,小于時用url-loader轉(zhuǎn)成base64,大于時使用file-loader加載圖片
- html-withimg-loader 在html中通過路徑加載圖片,在打包后能不受影響
- 圖片:如何把所有要打包的圖片都抽離到 img/目錄下 => url-loader => options => outputPath
- css: 如何把所要打包的css抽離到css/目錄下 => MiniCssExtractPlugin => filename => 'css/[name].css'
- 注意:當(dāng)圖片是babes64時即在limit范圍內(nèi)時,使用 outputPath 輸出到指定文件夾無效,因?yàn)闆]有靜態(tài)圖片資源而是base64
- 公共路徑
- 在output中設(shè)置 publicPath 即所有引用都會加上公共路徑前綴
- 如果只是想給圖片添加公共路徑的話: url-loader => options => publicPath:- 打包7多頁面應(yīng)用
- entry對象中使用不同的key值
- output中 => filename: '[name].[hash:8].js'
- html則要多次 new HtmlWebpckPlugin()
- 但是上面生成多個html會出現(xiàn)每個html都應(yīng)用所有的js,而不是只引用相對應(yīng)的js => 通過chunks:
- source-map源碼映射
- devtool: 'source-map' // 顯示行數(shù),產(chǎn)生map文件
- devtool: 'eval-source-amp' // 顯示函數(shù),不產(chǎn)生map文件
- 時時打包
- watch: true
- watchOptions: {
aggregateTimeout: 300, // 防抖
poll: 1000, // 每秒詢問1000次
ignored: /node_modules/
}
- aggregate: 總計,合計的意思
- poll: 投票數(shù)的意思
- https://www.cnblogs.com/chris-oil/p/8856020.html
- clean-wepack-plugin // https://github.com/johnagan/clean-webpack-plugin
- copy-webpack-plugin // https://github.com/webpack-contrib/copy-webpack-plugin
- BannerPlugin 是 webpack自帶的plugin,用于在js文件開頭注釋一些說明內(nèi)容
- new webpack.BannerPlugin({ banner: ' by woow.wu'})
- wepack設(shè)置代理
- devServer.proxy
- proxy: {
'/api': {
target: 'http://localhost:6000',
pathRewrite: {
'/api': '' // 將/api重寫成空字符串,即雖然前端請求時加了/api,但是真正代理到后端時,是去掉了/api的
}
}
}
- wepback中moke數(shù)據(jù)
- devServer.before // https://webpack.js.org/configuration/dev-server/#devserverproxy
- 這樣起始就不用在自己寫server了,并且也不用啟動node服務(wù),很方便
- devServer: {
before: function(app, server) {
app.get('/some/path', function(req, res) {
res.json({ custom: 'response' });
});
}
}
- resolve
- resolve.alias // 配置別名
- resolve.extensions // 引入資源時,省略后綴的加載順序
- alias: {
component: path.resolve(__dirname, 'src/component/')
},
- extensions: ['.js', '.less', '.css']
- 環(huán)境變量
- wepack.DefinePlugin // https://webpack.js.org/plugins/define-plugin/#root
- new webpack.DefinePlugin({
DEV: JSON.stringify('dev'),
FEATURE: JSON.stringify(true)
})
- webpack-merge 在base的基礎(chǔ)上定制化config文件
- module.exports = merge(base, devConfig)
- // https://webpack.js.org/guides/production/#setup
- happypack 多線程打包
- // https://github.com/amireh/happypack
- {
test: /\.js$/,
use: 'happypack/loader?id=js',
},
new HappyPack({
id: 'js',
use: [{
loader: 'babel-loader'
}]
}),
- tree-shaking
- tree-shaking能在打包的時候,自動去除沒用的代碼
- 比如一個sum,sub函數(shù),在import后只用到了sum,則sub函數(shù)在生產(chǎn)環(huán)境mode:production打包會被去除
- shaking: 是搖晃的意思
- 注意:
- 只有 import 和 export 語法是支持 tree-shaking的
- require和modue.exports并不支持 tree-shaking
- 注意:
- 當(dāng)使用了 optimize-css-assets-webpack-plugin后,tree-shaking會不生效,則需要下面的插件 terser-webpack-plugin
- terser-webpack-plugin
- terser: 是簡要的意思
- // https://segmentfault.com/q/1010000014940767
- 抽離公共組件和第三方組件,可以緩存,則不需要重復(fù)加載
- optimization => splitChunks => cacheGroups => vendors和commons
- priority: 是優(yōu)先的意思
- optimization: {
minimizer: {}, // 壓縮css和js的配置項(xiàng)
splitChunks: {
cacheGroups: {
commons: {
name: 'commons',
chunks: 'initial',
minChunks: 1,
priority: 10,
minSize: 0,
},
vendors: { // vendor是小販的意思
test: /node_modules/, // 范圍是node_modules中的第三方依賴,注意zhe
name: 'vendors', // 抽離出來的包的名字
chunks: 'initial', // 初始化加載的時候就抽離公共代碼
minChunks: 1, // 被引用的次數(shù)
priority: 11, // priority: 是優(yōu)先級的意思,數(shù)字越大表示優(yōu)先級越高
minSize: 0,
}
}
}
}
- 熱更新
- new webpack.HotModuleReplacementPlugin() // 熱更新
- new webpack.NameModulesPlugin() // 打印熱更新模塊的路徑
- 1. 首先在 devServer 配置中增加 hot: true,表示開啟熱更新
- 2. 在plugins數(shù)組中 new new webpack.HotModuleReplacementPlugin()
- 3. 在入口js文件中:
- if (module.hot) { // 如果開啟了熱更新
module.hot.accept('./component/index.js', function () { // 路徑中的js文件改變,則執(zhí)行回調(diào)函數(shù)
const res = require('./component/index.js')
console.log(res.default.a, '模塊熱更新, 最新的a值')
})
}
- 懶加載
- @babel/plugin-syntax-dynamic-import // 語法動態(tài)引入插件
- syntax: 是語法的意思
- dynamic:是動態(tài)的意思
- import只能用在頂部報錯:解決需要安裝 babel-eslint 插件
- 安裝,babel-eslint插件,并且在 .eslintrc.json中做如下配置
- {
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module",
"allowImportExportEverywhere": true
},
"rules": {
"indent": "off"
},
"env": {}
}
- https://stackoverflow.com/questions/39158552/ignore-eslint-error-import-and-export-may-only-appear-at-the-top-level
- 具體使用懶加載如下:
- // 懶加載
const button = document.createElement('button')
button.addEventListener('click', () => {
// console.log('button clicked')
import('./component/index.js').then(res => {
console.log(res.default.a)
})
})
button.innerHTML = 'button'
document.body.appendChild(button)
- 本質(zhì)上import().then()使用 jsonp實(shí)現(xiàn)的
wepback 打包優(yōu)化
1. exclude 和 include
- 使用 babel-loader 解析js文件時
- 如果在js中文件中,引用了第三方模塊,這個時候,時不需要再用 babel-loader 進(jìn)行編譯的,因?yàn)橐呀?jīng)打包過了的
- 所以添加 exclude: /node_modules/ 配置項(xiàng),排除node_modules文件夾
- 注意:include,exclude沒必要用于 (圖片) 相關(guān)的loader
- 原理:降低loader的使用頻率,從而提高webpack的打包速度
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
}
]
}
2. 在dev環(huán)境中,沒有必要使用 optimize-css-assets-webpack-plugin , mini-css-extract-plugin
- 盡量減少 plugin 的使用,選擇性能比較好的,官方推薦的plugin
3. resolve
- extensions
- resolve中的 extensions 中一般只對 js或者jsx這樣的邏輯型文件時,使用 extensions
- 因?yàn)槿绻押芏嗪缶Y都配置在這里,就會調(diào)用多次底層去判斷是否命中,消耗性能
- extensions: ['.js', '.jsx']
- 在引入 import A from 'component/A' 這里可以省略擴(kuò)展名 .js 或者 .jsx
- mainFiles
- mainFiles: ['index']
- 如果配置了index,則在引入的時候可以這樣寫 import a from './src/component/'
resolve: {
modules: ['node_modules'],
alias: {
component: path.resolve(__dirname, 'src/component/')
},
extensions: ['.js', '.jsx'] // extensions是擴(kuò)展的意思,擴(kuò)展名
mainFiles: ['index'] // 先找以index開頭的文件
}
4. DllPluguin和 DllReferencePlugin 提高打包速度 // 動態(tài)鏈接庫
- add-asset-html-webpack-plugin // 一個在html引入靜態(tài)文件的插件
- filepath: 需要引入的文件路徑
- webpack.DllPlugin() // 主要是用于生成映射文件
- name: '[name]' // 必須和output中的library相同,表示暴露到全局的庫的名稱
- path: path.resolve(__dirname, 'dll', '[name].manifest.json') // .manifest.json就是映射文件
- webpack.DllReferencePlugin() // 最新引入的是映射文件中的依賴,而不是在node_modules中去查找
- manifest: path.resolve(__dirname, 'dll', 'vendors.manifest.json')
- 原理:
- 使用splitChunks可以單獨(dú)抽離出第三方包,但是每次打包都會執(zhí)行抽離的過程
- 所以需要抽離并且第一次打包后,第二次打包就不需要再打包抽離的包了
- 所以要經(jīng)過兩步:抽離 和 只打包一次
- 具體步驟:
- 1. 新建webpack.dll.js // 生成兩類文件,一個是js文件,一個是manifest.json映射文件
- 2. 在html中引入抽離出來的js文件 // 這里可以使用插件,add-asset-webpack-plugin
- 3. 在webpack.common.js中引用manifest.json文件
- 調(diào)試
- 在webpack.common.js中注釋掉 new wepback.DllReferencePlugin前后對比打包時間
- 代碼
webpack.dll.js
module.exports = {
mode: 'development',
entry: {
react: ['react', 'react-dom']'lodash', 'moment']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, 'dll'),
library: '[name]', // 暴露到全局的變量名
libraryTarget: 'var' // 以var的方式暴露
},
plugins: [
new webpack.DllPlugin({
name: '[name]', // name === library 必須一樣 (在manifest.json中對應(yīng)的各個包的名字)
path: path.resolve(__dirname, 'dll', '[name].manifest.json') // 生成在dll文件夾下的name.manifest.json文件
})
]
}
webpack.common.js
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, 'dll', '*.dll.js')
}),
new wepback.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dll', 'vendors.manifest.json')
})
// https://webpack.js.org/plugins/dll-plugin/#root
- 優(yōu)化
- 當(dāng)webpack.dll.js中的entry每個第三方包都單獨(dú)生成 .dll.js和 .manifest.json文件后,需要多次使用上面兩個插件
- 所有可以用 nodejs 的 fs.readdirSync(path)去獲取dll文件夾的所有文件數(shù)組
- 代碼如下:
const dllPlugins = []
fs.readdirSync(path.resolve(__dirname, 'dll')).forEach(item => {
console.log(item, 'item')
if (/\.dll\.js/.test(item)) {
dllPlugins.push(
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, 'dll', item)
})
)
}
if (/\.manifest\.json/.test(item)) {
dllPlugins.push(
new wepback.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dll', item)
})
)
}
})
plugins: [
...dllPlugins
]
5. happypack多線程打包
6. sourcmap調(diào)試映射文件的各種類型
7. 結(jié)合stats分析打包結(jié)果
手寫一個loader
- loader-utils
- this.query
- this.callback
- this.async
- resolveLoader // webpack配置對象,moudles屬性
前置知識:
1. loader函數(shù)中的第一個參數(shù)表示源代碼
2. 不能寫成箭頭函數(shù),因?yàn)樾枰ㄟ^ this 獲取很多api
3. 如何獲取loader中的配置參數(shù) options對象
- this.query // 指向的就是 options對象
// 如果 loader 中沒有 options,而是以 query 字符串作為參數(shù)調(diào)用時,this.query 就是一個以 ? 開頭的字符串
- 注意:
- this.query已經(jīng)廢棄,使用 loader-utils 中的 getOptions 來獲取 options對象
4. loader-utils
- 通過loader-utils中的 getOptions 獲取 loader的options配置對象
5. this.callback
- 參數(shù)
- 第一個參數(shù):err // Error 或者 null
- 第二個參數(shù):content // string或者buffer
- 第三個參數(shù):sourceMap // 可選,必須是一個可以被這個模塊解析的 source map
- 第四個參數(shù):meta //可選
- // https://www.webpackjs.com/api/loaders/#this-callback
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
6. this.async // 處理loader中有異步操作
- this.async()方法返回 this.callback
7. resolveLoader // webpack配置項(xiàng)
- modules: ['node_modules', './src/loaders']
// 在尋找loader的時候,先去node_modules文件夾中共尋找,沒找到再去'./src/loaders'文件夾中找
------------
代碼:replace-loader.js // 如下
------------
const loaderUtils = require('loader-utils')
module.exports = function (source) {
console.log(this.query, 'this.query')
const options = loaderUtils.getOptions(this) // 獲取laoder的options對象
console.log(options, 'loader-utils中的getOtions')
const callback = this.async(); // this.async()方法返回的是 this.callback
setTimeout(() => { // 注意這里的setTimeout的環(huán)境
const result = source.replace('guoqing', options.replacename) // 拿到options對對象中設(shè)置的replacename屬性
callback(null, result)
}, 1000);
// return source.replace('guoqing', this.query.name)
// return source.replace('guoqing', options.name)
}
------------
代碼:webpack.dev.js // 如下
------------
resolveLoader: {
modules: ['node_modules', './src/loaders'] // 先找node_module再找后面的文件夾
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'replace-loader.js', // 配置了resolveLoader的modules后,這里就直接寫了
options: {
replacename: '2019 new new good'
}
}
]
}
]