前言
重學Webpack4專題來了,本文有一種代碼片段的既視感。本專題有字節跳動@xfz投稿分享。
@xfz,字節跳動高級前端開發,負責ToB業務的設計、研發與維護,夢想做一名幼師
正文從這開始~~
安裝基礎依賴入口
// 單入口,SPA
entry: 'xx/xx.js'
// 多入口 MPA
entry: {
app: './src/app.js',
adminApp: './src/adminApp.js'
}
output指定打包後的輸出
output: {
path: path.resoluve(__diranme,'dist')
filename: '[name].js' // 單入口可以寫死文件名,多入口一定要使用佔位符[name],來自動生成多個文件
// filename: '[name].[chunkhash:5]]js'
// filename: '[name].[hash]js'
}
Loaderswepback開箱即用只支持JS和JSON兩種文件類型,通過Loader去支持其他文件類型並把他們轉化成有效的模塊,並且可以添加到依賴圖中
本身是一個函數,接受源文件作為參數,返迴轉換的結果
例如: babel/ts-loader、css/less/scss-loader、url/file-loader、raw-loader(將.txt文件以字符串的形式導入)、thread-loader(多進程打包js和css)
module: {
rules: [
{test: /.txt$/, use: 'raw-loader'},
{test: /.css$/, use: [
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
}
}
]}
]
}
解析ES6使用babel-loader,babel的配置文件.babelrc
安裝 babel-loader、@babel/core @babel/preset-env
// webpack.config
module: {
rules: [
{test: /.js?x$/, use: 'babel-loader', exclude: /node_modules/}
]
}
// .babelrc
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
// 各種插件
"@babel/propsoal-class-properties"
]
}
css/less/scss-loadercss-loader用於加載.css文件,並且轉換成 common 對象
style-loader 將樣式通過 style 標籤,插入到 head 中
// use: [loader1,loader2,loader3],loader的處理順序是 3>2>1,從後往前
module: {
rules: [
{
test: /.s?css$/,
use: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader
{
loader: 'css-loader',
options: {
importLoaders: 1,
// css模塊化使用
modules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
}
},
'postcss-loader',
'sass-loader'
],
include: [],
exclude: [
Root('src/components')
]
},
]
}
url/file-loader用於處理文件,圖片、字體、多媒體
url-loader 實現較小的圖片轉成base64,插入到代碼中,當超過限制的limit後,會自動降級到file-loader
{
test: /\.(png|jpg|jpeg|gif|eot|woff|woff2|ttf|svg|otf)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10 * 1024, // 10k
name: isDev ? 'images/[name].[ext]' : 'images/[name].[hash.[ext]',
publicPath: idDev ? '/' : 'cdn地址',
},
},
// prduction,用於圖片壓縮
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true
}
}
]
},
postcss-loader預處理器, autoprefixer(需要安裝)
rules: [
{test: /.css$/, use: [
{
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => {
require('autoprefixer')({
browsers: ['last 2 version', '>1%', 'ios7']
})
}
}
},
'sass-loader'
}
]
}
或者將postcss-loader的options放在根目錄的postcss.config.js中
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
'less-loader',
]
}
]
// postcss.config.js 和 package.json(或者.browserslistrc,推薦使用.browserslistrc)
// 安裝postcss-preset-env,autoprefixer
module.exports = {
loader: 'postcss-loader',
plugins: {
'postcss-preset-env': {
stage: 0,
features: {
'nesting-rules': true,
'autoprefixer': { grid: true },
''
}
}
}
};
// package.json
"browserslist": [
"last 2 version",
">1%",
"iOS >= 7"
]
// .browserslistrc
last 2 version
>1%
iOS >= 7
px自動轉rem(兩種方式)方式一:px2rem-loader與lib-flexible,頁面渲染時計算根元素的font-size,推薦使用:
cnpm i -D px2rem-loader
cnpm i -S lib-flexible
不推薦,將lib-flexible代碼拷貝到 html>head>script中使用raw-loader內聯lib-flexible
rules: [
{
test: /.css$/,
use: [
{
'style-loader',
'css-loader',
'postcss-loader',
{
loader: 'px2rem-loader',
options: {
remUnit: 75, // 一個rem等於多少px
remPrecision: 8 // px轉換成rem的小數位
}
}
'sass-loader',
}
]
}
方式二,使用postcss-loader與postcss-px2rem-exclude
module.exports = {
loader: 'postcss-loader',
plugins: {
'postcss-preset-env': {
stage: 0,
features: {
'nesting-rules': true,
'autoprefixer': { grid: true },
}
},
'postcss-px2rem-exclude': {
remUnit: 200,
exclude: /node_modules/i
}
}
};
plugins插件用於bundle文件的優化,資源管理和環境變量注入,作用於整個構建過程
plugins: [
new HtmlWpeckPlugin({})
]
webpack-dev-serverwebpack-dev-server,開啟本地伺服器,監聽文件變化後,熱更新頁面
不刷新瀏覽器而是熱更新,不輸出文件,而是放在內存中
配合 new.webpack.HotModuleReplacementPlugin() 或 react-hot-loader 插件使用
// package.json
webpack-dev-server mode=development -open
// config
module.exports = {
devServer: {
host: '0.0.0.0',
compress: true,
port: '3000',
contentBase: join(__dirname, '../dist'),//監聽的目錄,用於刷新瀏覽器
hot: true,
overlay: {
errors: true,
warnings: true
},
disableHostCheck: true,
publicPath: '/', // 設置時,要與output.publicPath保持一致
historyApiFallback: true,
// historyApiFallback: {
// rewrites: [from: /.*/, to: path.posix.join('/', // 'index.html')],
//}
proxy: {
'/api': 'http://localhost:8081',
}
//proxy: {
// '/api/*': {
// target: 'https://xxx.com',
// changeOrigin: true,
// secure: false,
// headers: {},
// onProxyReq: function(proxyReq, req, res) {
// proxyReq.setHeader('origin', 'xxx.com');
// proxyReq.setHeader('referer', 'xxx.com');
// proxyReq.setHeader('cookie', 'xxxxx');
// },
// onProxyRes: function(proxyRes, req, res) {
// const cookies = proxyRes.header['set-cookie'];
// cookies && buildCookie(cookies)
// }
// }
// }
},
}
webpack-dev-middleware將webpack輸出文件傳輸給伺服器,適用於靈活的定製場景
const express = requrie('express')
const webpack = require('webpack')
const webpackDevMiddleware = requrie('webpack-dev-middleware')
const app = express()
const config = require('./webpack.config.js')
const compiler = webpack(config)
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
}))
app.listen(3000)
熱更新原理mini-css-extract-plugin和optimize-css-assets-webpack-plugin提取css,建議使用contenthash
module: {
rules: [
{
test: /.s?css$/,
use: [
isDev ? 'style-loader' : iniCssExtractPlugin.loader
{
loader: 'css-loader',
options: {
importLoaders: 1,
// css模塊化使用
modules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
}
},
'postcss-loader',
'sass-loader'
]
},
]
},
plugins: [
// 提取css
new MiniCssExtractPlugin({
filename: 'styles/[name].[contenthash:5].css',
}),
// 壓縮css
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require("cssnano"),//需要安裝cssnano
cssProcessorPluginOptions: {
preset: [
'default',
{
discardComments: {
removeAll: true
}
}
]
},
canPrint: true
}),
]
html-webpack-pluginplugins: [
new HtmlWebpackPlugin({
// 自定義參數title傳遞到html中
// html中使用<title><%= htmlWebpackPlugin.options.title %></title>
// <script>let number = <%= htmlWebpackPlugin.options.number %><script>
number: 1,
title: '京程一燈CRM系統',
filename: 'index.html',
// chunks: ['index'] //用於多頁面,使用哪些chunk
template: resolve(__dirname, '/src/index.html'),
minify: {
minifyJS: true,
minifyCSS: true,
removeComments: true,
collapseWhitespace: true,
preserveLineBreak: false,
removeAttributeQuotes: true,
removeComments: false
}
}),
]
clean-webpack-plugin 或者使用 rimraf distplugins: [
new CleanWebpackPlugin()
]
modeMode 用來指定當前構建環境 development、production 和 none
設置 mode 可以使用 wepack內置函數,內部自動開啟一些配置項,默認值為 production
內置功能development:process.env.NODE_ENV為development,開啟NamedChunksPlugin 和 NameModulesPlugin
這兩個插件用於熱更新,控制臺列印路徑prodution:process.env.NODE_ENV為prodution.開啟 FlagDependencyUsagePlugin、ModuleConcatenationPlugin、NoEmitOnErrorsPlugin,OccurrentceOrderPlugin、SideEffectsFlagPlugin等
none:不開啟任何優化選項
watch文件監聽可以在webpack命令後加上 --watch 參數,或在webpack.config中設置watch:true原理:輪詢判斷文件的最後編輯時間是否變化
module.exports = {
// 默認false,不開啟
watch: true,
// 只有開啟時,watchOptions才有意義
watchOptions: {
// 忽略,支持正則
ignored: /node_modules/,
// 監聽到變化後等300ms再執行,默認300ms
aggregateTimeout: 300,
// 怕亂文件是否變化是通過不停詢問系統指定文件有沒變化實現的,默認每秒1000次
poll: 1000
}
}
文件指紋打包後輸出文件名後綴,也就是hash值
hash:和整個項目構建相關,只要項目中有一個文件修改,整個項目中的文件hash都會修改成統一的一個
chunkhash:和webpck打包的chunk有關,不同的entry會生成不同的chunkhash值(適用於js文件)
contenthash:根據文件內容定義hash,文件內容不變,則contenthash不變,用於批量更新(適用於css文件)
資源內聯頁面框架的初始化,比如flexible
上報相關打點
css內聯避免頁面閃動
代碼:
// raw-loader@0.5.1 內聯html片段,在template中弄
<!-- 內聯html -->
<%= require('raw-loader!./meta.html') %>
// raw-loader內聯js
<!-- 初始化腳本,例如flexible -->
<script><%= require('raw-loader!babel-loader!../node_modules/lib-flexible/flexible.js') %></script>
css內聯合方式一:style-loader
module: {
rules: [
{
test: /.s?css$/,
use: [
{
loader: 'style-loader',
options: {
injectType: 'singletonStyleTag', // 將所有style標籤合併成一個
}
}
'css-loader'
'postcss-loader',
'sass-loader'
]
},
]
},
方式二:html-inline-css-webpack-plugin
首先使用 mini-css-extract-plugin(而非 style-loader)將 css 提取打包成一個獨立的 css chunk 文件。然後使用 html-webpack-plugin 生成 html 頁面。最後使用 html-inline-css-webpack-plugin 讀取打包好的 css chunk 內容注入到頁面,原本 html-webpack-plugin 只會引入 css 資源地址,現在實現了 css 內聯
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HTMLInlineCSSWebpackPlugin = require("html-inline-css-webpack-plugin").default;
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader"
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
}),
new HtmlWebpackPlugin(),
new HTMLInlineCSSWebpackPlugin(),
],
}
style-loader vs html-inline-css-webpack-plugin請求成面:減少HTTP網絡請求數
多頁面應用 MPA解決方案:
// 例如 ./src/index/index.js 與 ./src/search/index.js
// path: './src/*/index.js'
const setMPA = filenames => {
const entry = {}, htmlWebpackPlugins = [];
const entryFiles = glob.sync(path.join(__dirname, filenames))
for(let item of entryFiles){
// (/\/([a-z\_\$]+)\/index.js$/)
const match = item.match(/src\(.*)/index\.js$/)
const pageName = match && match[1];
entry[pageName] = item
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: `src/${pageName}/index.html`,
filename: `${pageName}.html`,
chunks: ["runtime", "common", pageName],
minify: {
// ..
}
})
)
}
return {
entry,
htmlWebpackPlugins
}
}
entry: entry
plugin: [//].concat(htmlWebpackPlugins)
source-map開發環境:建議使用
首先在原始碼的列信息是沒有意義的,只要有行信息就能完整的建立打包前後代碼之間的依賴關係。因此不管是開發環境還是生產環境,我們都會選擇增加cheap基本類型來忽略模塊打包前後的列信息關聯。
其次,不管在生產環境還是開發環境,我們都需要定位debug到最最原始的資源,比如定位錯誤到jsx,coffeeScript的原始代碼處,而不是編譯成js的代碼處,因此,不能忽略module屬性
再次我們希望通過生成.map文件的形式,因此要增加source-map屬性
module.expors = {
// 開發,因為eval的rebuild速度快,因此我們可以在本地環境中增加eval屬性
devtool: 'cheap-module-eval-source-map'
// 生產
devtool: 'cheap-module-source-map'
}
代碼拆分基礎庫分離將react、react-dom基礎包通過cdn引入,不打入到bundle中
方式一: externals
// 1
module.exports = {
externals: {
react: 'React',
'react-dom': 'ReactDOM',
'react-router-dom': 'ReactRouterDOM',
mobx: 'mobx'
},
}
2. 在html模版中 script標籤引入對應的cdn地址
方式二:html-webpack-externals-plugin(推薦使用)
1.在html模版中 script標籤引入對應的cdn地址
2.plugins: [
new HtmlWebpackPlugin(),
new htmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: react的cdn地址,
global: 'React'
},
{
module: 'react-dom',
entry: react-dom的cdn地址,
global: 'ReactDOM'
},
{
module: 'react-router-dom',
entry: react-router-dom的cdn地址,
global: 'ReactRouterDOM'
},
// ...
]
})
]
方式三:webpack4 替代 CommonsChunckPlugin插件
module.exports = {
optimization: {
minimize: true,
runtimeChunk: {
name: 'manifest'
},
splitChunks: {
chunks: 'async', // async異步引入庫進行分離(默認),initial同步引入庫分離,all所有引入庫分離
minSize: 30000, // 抽離公共包最小的大小
maxSize: 0,
minChunks: 1, // 最小使用的次數
maxAsyncRequests: 5,
maxInitialRequests: 3,
name: true,
cacheGroups: {
// 提取基礎庫,不使用CDN的方式
//commons: {
// test: /(react|react-dom|react-router-dom)/,
// name: "vendors",
// chunks: 'all'
//},
// 提取公共js
commons: {
chunks: "all", // initial
minChunks: 2,
maxInitialRequests: 5,
minSize: 0,
name: "commons"
},
// vendors: {
// test: /[\\/]node_modules[\\/]/,
// priority: -10
// }
// 合併所有css
// styles: {
// name: 'style',
// test: /\.(css|scss)$/,
// chunks: 'all',
// minChunks: 1,
// enforce: true
// }
}
}
},
}
tree-shaking(靜態分析,不是動態分析)原理:利用ES6模塊的特點:
代碼擦除: uglify階段刪除無用代碼
Scope Hoisting代碼:
(function(module, __webpack_exports__,__webpack_require__){
__webpack_require__.r(__webpack_exports__);
})()
開啟scop hoisting
代碼分割splitChunck
動態引用
適用場景:抽離相同代碼到一個共享塊
腳本懶加載,使得初始下載代碼更小
懶加載JS腳本方式
// 配置.babelrc
"plugins": [
["@babel/plugin-syntax-dynamic-import"],
]
dist代碼通過window['webpackJsonp']來獲取對應腳本
ESlint//.eslint.js
/ 區分生產環境、開發環境
const _mode = process.env.NODE_ENV || 'production';
module.exports = {
"env": {
"browser": true,
"es6": true,
"node": true,
},
"globals": {
"$": true,
"process": true,
"dirname": true,
},
"parser": "babel-eslint",
"extends": "eslint:recommended",
"parserOptions": {
"ecmaFeatures": {
"jsx": true,
"legacyDecorators": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"react"
],
"rules": {
"no-console": "off",
"no-debugger": _mode==='development' ? 0 : 2,
"no-alert": _mode==='development' ? 0 : 2,
// "no-multi-spaces": "error",
"no-unused-vars": "off", // react中不適用
"no-constant-condition": "off",
"no-fallthrough": "off",
// "keyword-spacing": ["error", { "before": true} ], // 不生效,先注釋
// "indent": [
// "error",
// 2
// ],
"linebreak-style": [
"error",
"unix"
],
// "quotes": [
// "error",
// "single"
// ],
"semi": [0],
"no-unexpected-multiline": 0,
"no-class-assign": 0,
}
};
檢查eslint方式一: 安裝husky,增加npm script,適合老項目
"scripts": {
//"precommit": "eslint --ext .js --ext .jsx src/",
"precommit": "eslint lint-staged", // 增量檢查修改的文件
},
"lint-staged": {
//"src/**/*.js": [
// "eslint --ext .js --ext .jsx",
// "git add"
//]
"linters": {
"*.[js,scss]": ["eslint --fix", "git add"]
}
}
方式二:webpack與eslint結合,新項目
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: ['babel-loader', 'eslint-loader']
}
]
其實新項目中,可以將兩種方式同時使用
優化命令行日誌統計信息 stats,webpack屬性,這種方式不好
error-only:值發生錯誤時輸出
minimal:只在發生錯誤或有新的編譯時輸出
none:沒有輸出
normal:標準輸出,默認
verbose:全部輸出
// development
devServer: {
//
stats: 'errors-only'
}
// production
module.exports = {
stats: 'errors-only'
}
stats結合friendly-errors-webpack-plugin(推薦)
plugins: [
new FriendlyErrorsPlugin()
],
stats: 'errors-only'
構建異常和中斷處理wepback4之前的版本構建失敗不會跑出錯誤碼
node中的process.exit規範
主動捕獲錯誤,並處理構建錯誤
代碼如下:
plugins: [
function() {
// webpack3 this.plugin('done', (stats) => {})
// webpack4
this.hooks.done.tap('done', (stats) => {
if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf('--watch') == -1) {
console.log('build error');
// dosomething
process.exit(1);
}
})
}
]
關於本文作者:@xfz原文:https://juejin.im/post/6893511215768272909
本專題其他文章
重學webpack4之原理分析