前端繁榮發展,工程化已經成爲高級前端工程師的必不可少的條件之一,打包構建的發展從grunt
,fis
,glup
到rollup
,webpack
,Parcel
,技術手段變幻無窮,javascript
但其實不論任何一項技術或工具,都有五個階段,css
這五個階段越日後是越艱難,可是你越是日後深刻就越能透過表象看清它的本質,以在這快速變化的技術手段中站穩,以不變應萬變html
webpack如今是前端打包構建最流行的工具,那麼咱們就來好好了解一下它(webpack ^4.42.1)前端
首先梳理下本文要講到的內容vue
核心概念java
其餘經常使用配置node
優化手段react
配置總結webpack
webpack原理css3
loader編寫
plugin編寫
下面逐個介紹
定義打包的入口
使用示例
// 簡寫 module.exports = { entry: './src/index.js', } // 多入口 module.exports = { entry: { index: './src/index.js', list: './src/index.js', }, } 複製代碼
編譯後文件輸出到磁盤的相關配置
// 簡寫 module.exports = { output: { filename: '[name]_[chunkhash:8].js' //單個文件名可直接指定,多入口利用佔位符保證文件名統一 path: path.join(__dirname, '../dist') // 寫入文件磁盤路徑 publicPath: 'http://cdn.example.com/assets/' //資源使用 CDN ,給全部文件引入模版文件時加上路徑前綴 }, } 複製代碼
佔位符
webpack 原生只支持js 和json,利用loader,對不一樣文件類型支持,轉換成有效的模塊 簡單示例
module.exports = { module: { rules:[ { test: /\/.txt$/, // 指定匹配規則 use: 'babel-loader' // 指定使用的loader名稱 } ] } } 複製代碼
下面介紹幾種文件類型的處理以及經常使用的loader
babel-loader
:js默認是不支持es6 和jsx語法的,.babelrc
文件: 設置具體支持的屬性方法{ test: /\.(j|t)sx?$/, use: 'babel-loader', exclude: /node_modules/ }, 複製代碼
style-loader
:將樣式經過style
標籤插入模版文件的head當中css-loader
: 用於加載.css文件 而且轉換成commonjs 對象{ test: /.css$/, use: [ 'style-loader', 'css-loader', ], }, 複製代碼
less-loader
:less轉換成css,{ test: /.css$/, use: [ 'style-loader', 'css-loader', { loader: 'less-loader', options: { // 可配置屬性,修改變量的值,通常利用來修改UI庫的主題樣式,如下是antd主題樣式配置 modifyVars: { '@primary-color': '#ec7259', }, javascriptEnabled: true, }, }, ], }, 複製代碼
file-loader
:解析圖片, 字體等url-loader
:也可處理圖片和字體,,並可設置較小資源自動base64{ test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, use: [ { loader: 'url-loader', options: { limit: 8192, name: 'static/img/[name].[hash:8].[ext]',// [ext] 文件的後綴名 }, }, ], }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, loader: 'url-loader', options: { limit: 8192, name: 'static/fonts/[name].[hash:8].[ext]', }, }, 複製代碼
px2rem-loader
: 把px轉換成rem,配合lib-flexible使用{ loader: 'px2rem-loader', options: { remUnit: 75, // 1rem=多少像素 remPrecision: 8, // rem的小數點後位數 } } 複製代碼
postcss-loader
:用於瀏覽器適配,某些css3屬性瀏覽器不支持須要加前綴,它會自動針對不一樣瀏覽器加不一樣的屬性前綴{ loader: 'postcss-loader', options: { plugins: () => [autoprefixer()], }, }, 複製代碼
plugins
屬性傳入new
實例 簡單示例const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { plugin: { new webpack.ProgressPlugin(), new HtmlWebpackPlugin({template: './src/index.html'}) } } 複製代碼
下面介紹幾種經常使用的plugin
new HtmlWebpackPlugin({ filename: '../dist/template/index.html', // 指定生成的模版文件名及路徑 template: path.join(__dirname, '../src/template/index.html'), // 指定要使用的模版文件 inject: true, // 指定的chunk會自動注入html文件中 chunks: ['index'], //指定生成的html要使用的chunk minify: { // 代碼的最小化輸出 collapseWhitespace: true, // 刪除空格,可是不會刪除SCRIPT、style和textarea中的空格 preserveLineBreaks: false, // 是否保留換行符 minifyCSS: true, // css壓縮 minifyJS: true, // js壓縮 removeComments: true, // 刪除註釋,可是會保留script和style中的註釋 }, }), 複製代碼
rimraf dist
new CleanWebpackPlugin(), 複製代碼
new MiniCssExtractPlugin({ filename: '[name]_[contenthash:8].css' }), 複製代碼
new OptimizeCssAsssetePlugin({ assetNameRegExp: /\.css$/g, //文件匹配 cssProcessor: require('cssnano') // cssnano 壓縮和優化的css插件 }), 複製代碼
舉例:
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin') plugins: [ new HtmlWebpackExternalsPlugin({ externals:[ { module: 'react', entry: 'https://cdn.cn/16.8.0/react.min.js', global: 'React', }, { module: 'react-dom', entry: 'https://cdn.cn/16.8.0/react-dom.min.js', global: 'ReactDOM', } ] }), 複製代碼
development
: 開發模式,production
: 生產模式,none
: 無webpack會針對不一樣環境直接作一些優化工做,例如production模式下會進行,tree-shaking
和scope-hosting
下面優化會詳細介紹
遠古時期,咱們作前端開發時,寫一個html文件,在瀏覽器打開它查看效果,修改時,需手動更新,
後來咱們使用熱更新,webpack低版本,不提供熱更新的支持,咱們使用插件http-proxy-middleware
和webpack-hot-middleware
,實現熱更新,配置比較麻煩
最後webpack把熱更新集成在內部,就成了devServer
簡單配置以下
devServer: { historyApiFallback: true, // 單頁面程序 刷新瀏覽器會出現404,緣由是它經過這個路徑(好比: /search/list)來訪問後臺,因此會出現404,而把historyApiFallback設置爲true那麼全部的路徑都執行index.html host: '127.0.0.1', // 域名 open: true, //支持自動打開瀏覽器 hot: true, // 模塊熱替換,在前端代碼變更的時候無需整個刷新頁面,只把變化的部分替換掉 inline: false, // inline選項會爲入口頁面添加「熱加載」功能,即代碼改變後從新加載頁面 port: 8080, // 端口 proxy: proxyConfig._proxy, // 代理後端服務,舉例:可本地調試測試接口 before(app) { // 其餘中間件以前, 提供執行自定義中間件 apiMocker(app, path.resolve('./mocks/mock.js'), // 舉例:可用來作mock數據 proxyConfig._proxy); }, }, 複製代碼
首先來看下簡單的流程示意圖
webpack compile
將JS編譯成bundle.jsHMR server
將熱更新文件輸出給HMR Runtime,HMR -> HotModuleReplacement(熱模塊替換)Bundle server
提供文件在瀏覽器的訪問HMR Runtime
注入瀏覽器,更新文件的變化,使瀏覽器 和 服務器創建一個連接(websocket)bundle.js
構建輸出的文件啓動階段
1 -> 2 -> 3
初始代碼通過webpack compiler編譯進行打包
編譯好的文件傳輸給bundle server, 它就至關於一個服務器,它使文件以server的方式讓瀏覽器訪問
files
-> webpack Compiler
-> Bundle Sever
-> bundle.js
更新階段
1 -> 4 -> 5 -> 6
file文件發生變化,通過webpack compiler編譯
編譯好的文件傳輸給HMR server,通知瀏覽器端的HMR Runtime(一般以JSON形式傳輸)
HMR Runtime 更新代碼,實現無刷新改變頁面內容
files
-> webpack Compiler
-> HMR server
-> HMR Runtime
-> code
介紹幾個經常使用的屬性用法
alias
建立 import 或 require 的別名,來確保模塊引入變得更簡單extensions
自動解析肯定的擴展mainFileds
當從 npm 包中導入模塊時,決定在 package.json 中使用哪一個字段導入模塊moudles
告訴 webpack 解析模塊時應該搜索的目錄舉例:
// webpack 配置文件 resolve: { alias: { Util: path.resolve(__dirname, 'src/util/'), }, mainFileds: ['main'], extensions: ['.js', '.jsx', '.json'], moudles: [path.resolve(__dirname, 'node_modules')] }, //業務文件 component.js import Utility from '../../util/utility.js'; // 簡化寫法(不用寫文件路徑前綴,也不用寫引用文件的擴展名) import Utility from 'Util/utility'; 複製代碼
splitChunks
,代替以前的CommonsChunkPlugin,公共資源分離vendors
chunks屬性特別說明
splitChunks
公共文件分離commons
runtimeChunk
舉例:
optimization: { runtimeChunk: { name: 'manifest', }, splitChunks: { minSize: 50000 // 分離的包的體積大小 cacheGroups: { vendors: { test: /(react|react-dom)/, //正則匹配要分離的文件 name: 'vendors', chunks: 'all', // 肯定對何種引入方式的文件進行分離 minChunks: 1, // 最小使用的次數 priority: 10, // 多個緩存組時,須要有優先級排列,優先使用哪一個進行分離 }, commons: { // 分離公共文件 name: 'commons', chunks: 'all', minChunks: 2, priority: 5, }, }, }, }, 複製代碼
關鍵字定義
eval
模塊都使用 eval() 包裹執行,而且都有 //@ sourceURL(指向的是原文件index.js,調試的時候,根據sourceUrl找到的index.js文件的)source map
產生.map文件(這個map文件會和原始文件作一個映射,調試的時候,就是經過這個.map文件去定位原來的代碼位置的 )cheap
不包含列的信息,(假如代碼運行出現了錯誤,控制檯報出了,error,咱們點擊定位到具體源碼的時候,就只能定位到行,而不能定位到具體的列)inline
.map文件做爲dataUrl嵌入到打包文件,而不單獨生成幾種關鍵字進行組合就造成了具體的用法
不一樣用法對構建速度是有影響的,基本狀況你越清晰容易的看到原始的代碼,構建速度就越慢,調試的方便性和構建速度上你們能夠本身權衡一下
共有13種用法,詳細的請看官方文檔
舉例
// 開發環境 devtool: 'cheap-module-eval-source-map' // 原始源代碼(僅限行) // 生產環境,通常不進行設置 複製代碼
構建統計信息
使用舉例
// 構建完成後會生成json文件,顯示構建的一些信息,時間,各模塊的體積等 scripts: { 'build: stats': 'webpack --config build/webpack.prod.config.js --json > stats.json', } 複製代碼
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin') const smp = new SpeedMeasureWebpackPlugin(); module.exports = smp.wrap(merge(webpackConfigBase, webpackConfigProd)); 複製代碼
圖中咱們看到了每一個插件和loader的耗時狀況, 若是耗時較長,會以紅字提示,咱們就能夠具體分析那個地方爲何時間長,能夠用別的插件替換之類的去作構建速度優化
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); plugins: [ new BundleAnalyzerPlugin({ analyzerPort: 8919 //打包構建後體積分析圖展現的窗口 }), ], 複製代碼
運行打包命令,體積分析示意圖會自動打開在8919窗口
圖中咱們能夠看到moment插件佔用的空間很大,咱們能夠對它進行優化
new webpack.IgnorePlugin(/\.\/locale/, /moment/), // compoent import 'moment/locale/zh-cn'; moment.locale('zh-cn'); 複製代碼
Dead Code(什麼是死碼呢?)
ES6 module 特色:
ES6模塊依賴關係是肯定的,和運行時的狀態無關,能夠進行可靠的靜態分析,這就是tree-shaking的基礎,讓死碼清除成爲了可能
靜態分析就是不執行代碼,僅僅從字面的意思上對代碼進行分析,ES6以前的模塊化,好比咱們能夠動態require一個模塊,只有執行後才知道引用的什麼模塊,這個就不能經過靜態分析去作優化,特別說明import()
動態引入也是不支持的
引用的文件tools.js
export default 'Hello World'; 複製代碼
入口文件index.js
import str from './tools'; console.log(str); 複製代碼
未開啓scope-hoisting
編譯後文件
/* 0 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony import */ var _tools__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); console.log(_tools__WEBPACK_IMPORTED_MODULE_0__["default"]); /***/ }), /* 1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony default export */ __webpack_exports__["default"] = ('Hello World'); /***/ }) 複製代碼
能夠看到
開啓scope-hoisting
編譯後文件
/* 0 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; // ESM COMPAT FLAG __webpack_require__.r(__webpack_exports__); /* harmony default export */ var tools = ('Hello World'); console.log(tools); /***/ }) 複製代碼
能夠看到
happypack
和thread loader
給咱們提供了方案因爲happypack做者再也不維護此項目,同時二者原理大體一致,咱們就主要介紹
thread loader
rules: [ { test: /.js$/, use: [ { loader: 'thread-loader', options: { workers: 3, // 產生的 worker 的數量,默認是 cpu 的核心數 } } ] }, ] 複製代碼
注意:thread loader 只有在項目龐大複雜的時候才能顯著的凸顯效果,若是是中小型項目沒有必要使用
日誌上報
plugins: [ // 主動捕獲構建錯誤 function () { this.hooks.done.tap('done', (stats) => { if (stats.compilation.errors && process.argv.indexOf('--watch' == -1)) { console.log('error', stats.compilation.errors); // 能夠作一個構建系統的日誌,在此處上報錯誤緣由 process.exit(12); // 自定義錯誤code碼 } }); }, ], 複製代碼
terser-webpack-plugin
開啓 paralleluglify-webpack-plugin
也支持並行壓縮,但不支持es6,不作具體介紹,有興趣的同窗自行查詢optimization: { minimizer: [ new TerserPluginWebpack({ parallel: 4, // 開啓 不主動指定的話,默認數值是當前電腦cpu數量的2倍減1 }) ], } 複製代碼
dll-plugin
進行分包,dllreferenceplugin
對mainfest.json(對分離包的描述)引用,將預編譯的模塊加載進來add-asset-html-webpack-plugin
把生成文件插入模版舉例
// package.json "scripts": { "dll": "webpack --config build/webpack.dll.js" // 打包前只需執行一次,只要基礎包不變則不須要執行 }, // webpack.dll.js const path = require('path'); const webpack = require('webpack'); module.exports = { entry: { // 指定要分離的包 library: [ // 基礎包 'react', 'react-dom', ], }, output: { filename: "[name]_[hash].dll.js", path: path.resolve(__dirname, './library'), library: "[name]_[hash]", //包名稱 注意此名需和下面的DllPlugin,name名一致 }, plugins: [ new webpack.DllPlugin({ name: "[name]_[hash]", path: path.resolve(__dirname, './library/[name].json') }) ] } // webpack.prod.js plugins: [ ... // 將給定的 JS文件添加到 webpack 配置的文件中,並插入到模版文件中 new AddAssetHtmlPlugin({ filepath: path.resolve(__dirname, '../build/library/*.dll.js'), }), // new webpack.DllReferencePlugin({ manifest: require('../build/library/library.json') }), ] 複製代碼
hard-source-webpack-plugin
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); module.exports = { plugins: [ ... new HardSourceWebpackPlugin() ] ]} 複製代碼
總結
vue-cli
和create-react-app
中並無使用dllhard-source-webpack-plugin
替換dll
舉例
const PATHS = { src: path.join(__dirname, '../src') } plugin: [ new PurgecssPlugin({ paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }), // 注意是絕對路徑匹配 }), ] 複製代碼
{ test: /.(png|jpg|gif|jpeg)$/, use: [ { loader: 'url-loader', options: { limit: 8192, name:'[name]_[hash:8].[ext]' } }, { loader: 'image-webpack-loader', options: { mozjpeg: { progressive: true, quality: 65 }, optipng: { enabled: false, }, pngquant: { quality: [0.65, 0.90], speed: 4 }, gifsicle: { interlaced: false, }, webp: { quality: 75 } } }, ], }, 複製代碼
https://polyfill.io/v3/polyfill.min.js
https://polyfill.alicdn.com/polyfill.min.js?features=Promise
咱們以實際目標爲導向收集整理一下經常使用webpack配置要作什麼事情
小結
雖說了不少關於webpack的配置和優化,但咱們仍是要根據項目的複雜程度,公司&項目的具體狀況,處理遺留代碼的難度,來選擇性的處理, 有時若是強行使用反而得不償失
固然最後要記住一切的技術都只是工具,都要以賦能業務,價值產出爲目標,打包工具的目標就是
關於webpack原理
和loader&plugin
請看 webpack4配置到優化到原理(下)