1. 程式人生 > >webpack入門級 - 從0開始搭建單頁專案配置

webpack入門級 - 從0開始搭建單頁專案配置

## 前言 webpack 作為前端最知名的打包工具,能夠把散落的模組打包成一個完整的應用,大多數的知名框架 cli 都是基於 webpack 來編寫。這些 cli 為使用者預設好各種處理配置,使用多了就會覺得理所當然,也就不在意是內部是如何配置。如果脫離 cli 開發,可能就無從下手了。 最近在開發一些單頁專案時,出於需求便開始從頭搭建專案配置,本文主要分享搭建時用到的配置。 ## 準備工作 快速生成 package.json: ```bash npm init -y ``` 必不可少的 webpack 和 webpack-cli: ```bash npm i webpack webpack-cli -D ``` ### 入口、出口 webpack 的配置會統一放到配置檔案中去管理,在根目錄下新建一個名為 `webpack.config.js` 的檔案: ```js const path = require('path') module.exports = { entry: { main: './src/js/main.js' }, output: { // filename 定義打包的檔名稱 // [name] 對應entry配置中的入口檔名稱(如上面的main) // [hash] 根據檔案內容生成的一段隨機字串 filename: '[name].[hash].js', // path 定義整個打包資料夾的路徑,資料夾名為 dist path: path.join(__dirname, 'dist') } } ``` `entry` 配置入口,可配置多個入口。webpack 會從入口檔案開始尋找相關依賴,進行解析和打包。 `output` 配置出口,多入口對應多出口,即入口配置多少個檔案,打包出來也是對應的檔案。 修改 package.json 的 script 配置: ```json "scripts": { "build": "webpack --config webpack.config.js" } ``` 這樣一個最簡單的配置就完成了,通過命令列輸入 `npm run build` 就可以實現打包。 ### 配置項智慧提示 webpack 的配置項比較繁雜,對於不熟悉的同學來說,如果在輸入配置項能夠提供智慧提示,那開發的效率和準確性會大大提高。 預設 VSCode 並不知道 webpack 配置物件的型別,通過 import 的方式匯入 webpack 模組中的 Configuration 型別後,在書寫配置項就會有智慧提示了。 ```js /** @type {import('webpack').Configuration} */ module.exports = { } ``` ![](https://s3.ax1x.com/2020/12/13/rmYZIH.gif) ### 環境變數 一般開發中會分開發和生產兩種環境,而 webpack 的一些配置也會隨環境的不同而變化。因此環境變數是很重要的一項功能,使用 `cross-env` 模組可以為配置檔案注入環境變數。 安裝模組 `cross-env`: ```bash npm i cross-env -D ``` 修改 package.json 的命令傳入變數: ```json "scripts": { "build": "cross-env NODE_ENV=production webpack --config webpack.config.js" } ``` 在配置檔案裡,就可以這樣使用傳入的變數: ```js module.exports = { devtool: process.env.NODE_ENV === 'production' ? 'none' : 'source-map' } ``` 同理,在專案的 js 內也可以使用該變數。 ### 設定 source-map 該選項能設定不同型別的 `source-map` 。程式碼經過壓縮後,一旦報錯不能準確定位到具體位置,而 `source-map` 就像一個地圖, 能夠對應原始碼的位置。這個選項能夠幫助開發者增強除錯過程,準確定位錯誤。 為了體驗它的作用,我在原始碼中故意輸出一個不存在的變數,模擬線上錯誤: ![](https://s3.ax1x.com/2020/12/16/rl23b6.jpg) 在預覽時,觸發錯誤: ![](https://s3.ax1x.com/2020/12/16/rl2lK1.jpg) 很明顯錯誤的行數是不對應的,下面設定 `devtool` 讓 webpack 在打包後輸出 `source-map` 檔案,用於定位錯誤。 ```js module.exports = { devtool: 'source-map' } ``` 再次觸發錯誤,`source-map` 檔案起作用準確定位到程式碼錯誤的行數。 ![](https://s3.ax1x.com/2020/12/16/rl21Dx.jpg) `source-map` 一般只在開發環境用於除錯,上線時絕對不能帶有 `source-map` 檔案,這樣會暴露原始碼。下面通過環境變數來正確設定 `devtool` 選項。 ```js module.exports = { devtool: process.env.NODE_ENV === 'development' ? 'source-map' : 'none' } ``` `devtool` 的可選項很多,它的品質和生成速度有關聯,比如只定位到某個檔案,或者定位到某行某列,相應的生成速度會快或更慢。可以根據你的需求進行選擇,更多可選值請檢視 [webpack 文件](https://webpack.docschina.org/configuration/devtool/) ## loader 與 plugin loader 與 plugin 是 webpack 的靈魂。如果把 webpack 比作成一個食品加工廠,那麼 loader 就像很多條流水線,對食品原料進行加工處理。plugins 則是在原有的功能上,新增其他功能。 ### loader 基本用法 loader 配置在 module.rules 屬性上。 ```js module.exports = { module: { rules: [] } } ``` 下面來簡單瞭解下 loader 的規則。一般常用的就是 `test` 和 `use` 兩個屬性: 1. `test` 屬性,用於標識出應該被對應的 loader 進行轉換的某個或某些檔案。 2. `use` 屬性,表示進行轉換時,應該使用哪個 loader。 ```js rules: [ { test: /\.css$/, use: ['css-loader'] } ] // 也可以寫為 rules: [ { test: /\.css$/, loader: 'css-loader' } ] ``` 上面例子是匹配以 .css 結尾的檔案,使用 css-loader 解析。 當有些 loader 可傳入配置時,可以改為物件的形式: ```js rules: [ { test: /\.css$/, user: [ { loader: 'css-loader', options: {} } ] } ] ``` 最後需要記住多個 loader 的執行是嚴格區分先後順序的,從右到左,從下到上。 ### plugin 基本用法 plugin 配置在 plugins 屬性上。 ```js module.exports = { plugins: [] } ``` 一般 plugin 會暴露一個建構函式,通過 new 的方式使用。plugin 的引數則在呼叫函式時傳入。plugin 命名一般是將按照原名轉為大駝峰。 ```js const { CleanWebpackPlugin } = require('clean-webpack-plugin') module.exports = { plugin: [ new CleanWebpackPlugin() ] } ``` ### 生成 html 檔案 沒有經過任何配置的 webpack 打包出來只有 js 檔案,使用外掛 `html-webpack-plugin` 可以自定義一個 html 檔案作為模版,最終 html 會被打包到 dist 中,而 js 也會被引入其中。 安裝 `html-webpack-plugin`: ```bash npm i html-webpack-plugin -D ``` 配置 plugins: ```js const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { plugins: { // 使用html模版 new HtmlWebpackPlugin({ // 配置html標題 title: 'home', // 模版路徑 template: './src/index.html', // 壓縮 minify: true }) } } ``` 此時想要配置的標題生效還需要在 html 中為 title 標籤插值: ```html <%= htmlWebpackPlugin.options.title %> ``` 除了 title 外,還可以配置諸如 meta 標籤等。 #### 多頁面 上面的使用方法,在打包後只會有一個 html。對於多頁面的需求其實也很簡單,有多少個頁面就 new 幾次 `htmlWebpackPlugin`。 但是需要注意一點,入口配置的 js 是會被全部引入到 html 的。如果想某個 js 對應某個 html,可以配置外掛的 chunks 選項。而且需要配置 filename 屬性,因為 filename 屬性預設是 index.html,同名會被覆蓋。 ```js module.exports = { entry: { main: './src/js/main.js', news: './src/js/news.js' }, output: { filename: '[name].[hash].js', path: path.join(__dirname, 'dist') }, plugins: { new HtmlWebpackPlugin({ template: './src/index.html', filename: 'index.html', minify: true, chunks: ['main'] }), new HtmlWebpackPlugin({ template: './src/news.html', filename: 'news.html', minify: true, chunks: ['news'] }), } } ``` ### es6轉es5 安裝 babel 的相關模組: ```bash npm i babel-loader @babel/core @babel/preset-env -D ``` `@babel/core` 是 babel 的核心模組,`@babel/preset-env` 內預設不同環境的語法轉換外掛,預設將 es6 轉 es5。 根目錄下建立 `.babelrc` 檔案: ``` { "presets": ["@babel/preset-env"] } ``` 配置 loader: ```js rules: [ { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' } ] ``` ### 解析 css 安裝 loader: ```bash npm i css-loader style-loader -D ``` `css-loader` 只負責解析 css 檔案,通常需要配合 `style-loader` 將解析的內容插入到頁面,讓樣式生效。順序是先解析後插入,所以 `css-loader` 放在最右邊,第一個先執行。 ```js rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'] } ] ``` 但要注意最終打包出來的並不是css檔案,而是js。它是通過建立 style 標籤去插入樣式。 ![](https://s3.ax1x.com/2020/12/13/rmYreU.png) ### 分離css 經過上面的 css 解析,打包出來的樣式會混在 js 中。某些場景下,我們希望把 css 單獨打包出來,這時可以使用 `mini-css-extract-plugin` 分離 css。 安裝 `mini-css-extract-plugin`: ```bash npm i mini-css-extract-plugin -D ``` 配置 plugins: ```js const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { plugins: [ new MiniCssExtractPlugin({ filename: 'css/[name].[hash].css', chunkFilename: 'css/[id].[hash].css' }) ] } ``` 配置 loader: ```js rules: [ { test: /\.css$/, use: [ // 插入到頁面中 'style-loader', { loader: MiniCssExtractPlugin.loader, options: { // 為外部資源(如影象,檔案等)指定自定義公共路徑 publicPath: '../', } }, 'css-loader', ] }, ] ``` 經過上面配置後,打包後 css 就會被分離出來。 ![](https://s3.ax1x.com/2020/12/13/rmYBLT.png) 但要注意如果 css 檔案不是很大的話,分離出來效果可能會適得其反,因為這樣會多一次檔案請求,一般來說單個 css 檔案超過 200kb 再考慮分離。 ### css瀏覽器相容字首 安裝相關依賴: ```jbash npm i postcss postcss-loader autoprefixer -D ``` 專案根目錄下新建 `postcss.config.js`: ```js module.exports = { plugins: [ require('autoprefixer')() ] } ``` `package.json` 新增 `browserslist` 配置: ```json { "browserslist": [ "defaults", "not ie < 11", "last 2 versions", "> 1%", "iOS 7", "last 3 iOS versions" ] } ``` 最後配置 `postcss-loader`: ```js rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader', 'postcss-loader' ] } ] ``` 處理前後對比: ![](https://s3.ax1x.com/2020/12/13/rmYqfA.png) ### 壓縮 css webpack 內部預設只對 js 檔案進行壓縮。css 的壓縮可以使用 `optimize-css-assets-webpack-plugin` 來完成。 安裝 `optimize-css-assets-webpack-plugin`: ```bash npm i optimize-css-assets-webpack-plugin -D ``` 配置 plugins: ```js const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin') module.exports = { plugins: [ new OptimizeCssAssetsWebpackPlugin() ] } ``` 壓縮結果: ![](https://s3.ax1x.com/2020/12/15/rMcuZD.png) ### 解析圖片 專案中肯定少不了圖片,對於圖片資源,`webpack` 也有相應的 `url-loader` 來解析。`url-loader` 除了解析圖片外,還可以將比較小的圖片可以轉為base64,減少線上對圖片的請求。 安裝 `url-loader`: ```bash npm i url-loader -D ``` 配置 loader: ```js rules: [ { test: /\.(jpg|png|jpeg|gif)$/, use: [{ loader: 'url-loader', // 小於50k的圖片轉為base64,超出的打包到images資料夾 options: { limit: 1024 * 50, outputPath: './images/', pulbicPath: './images/' } }] } ] ``` 配置完成後,只需要在入口檔案內引入圖片使用,`webpack` 就可以幫助我們把圖片打包出來了。 但有時候,圖片連結是直接寫到 html 中,這種情況 `url-loader` 無法解析。不慌,使用 `html-loader` 能完成這項需求。 ```js rules: [ { test: /\.(html)$/, use: [{ loader: 'html-loader', options: { // 壓縮html模板空格 minimize: true, attributes: { // 配置需要解析的屬性和標籤 list: [{ tag: 'img', attribute: 'src', type: 'src', }] }, } }] }, ] ``` 注意這裡 `html-loader` 只是起到解析的作用,需要配合 `url-loader` 或者 `file-loader` 去使用。也就是說解析模板的圖片連結後,還是會走上面所配置的 `url-loader` 的流程。 還有一點,使用 `html-loader` 後, `html-webpack-plugin` 在 html 中的插值會失效。 ### 其他型別資源解析 解析其他資源和上面差不多,不過這裡用到的是 `file-loader`。`file-loader` 和 `url-loader` 主要是將檔案上的 `import` / `require()` 引入的資源解析為url,並將該資源傳送到輸出目錄,區別在於 `url-loader` 能將資源轉為 base64。 安裝 `file-loader`: ```bash npm i file-loader -D ``` 配置 loader: ```js rules: [ { test:/\.(mp3)$/, use: [{ loader: 'file-loader', options: { outputPath: './music/', pulbicPath: './music/' } }] }, { test:/\.(mp4)$/, use: [{ loader: 'file-loader', options: { outputPath: './video/', pulbicPath: './video/' } }] } ] ``` 上面只是列舉了部分,需要解析其他型別資源,參照上面的格式新增配置。 解析 html 中的其他型別資源也和上面同理,使用 `html-loader` 配置物件的標籤和屬性即可。 ### devServer 提高開發效率 每次想執行專案時,都需要 build 完再去預覽,這樣的開發效率很低。 官方為此提供了外掛 `webpack-dev-server`,它可以本地開啟一個伺服器,通過訪問本地伺服器來預覽專案,當專案檔案發生變化時會熱更新,無需再去手動重新整理,以此提高開發效率。 安裝 `webpack-dev-server`: ```bash npm i webpack-dev-server -D ``` 配置 `webpack.config.js`: ```js const path = require('path') module.exports = { devServer: { contentBase: path.join(__dirname, 'dist'), // 預設 8080 port: 8000, compress: true, open: true, } } ``` `webpack-dev-server` 用法和其他外掛不同,它可以不新增到 plugins,只需將配置新增到 `devServer` 屬性下即可。 新增啟動命令 `package.json`: ```json { "scripts": { "dev": "cross-env NODE_ENV=development webpack serve" }, } ``` 命令列執行 `npm run dev`,就可以感受到飛一般的體驗。 ### 複製檔案到 dist 對於一些不需要經過解析的檔案,在打包後也想將它放到 dist 中,可以使用 `copy-webpack-plugin`。 安裝 `copy-webpack-plugin`: ```bash npm i copy-webpack-plugin -D ``` 配置 plugins: ```js const CopyPlugin = require('copy-webpack-plugin') module.exports = { plugins: [ new CopyPlugin({ patterns: [ { // 資源路徑 from: 'src/json', // 目標資料夾 to: 'json', } ] }), ] } ``` ### 打包前清除舊 dist 打包後文件一般都會帶有雜湊值,它是根據檔案的內容來生成的。由於名稱不同,可能會導致 dist 殘留有上一次打包的檔案,如果每次都手動去清除顯得不那麼智慧。利用 `clean-webpack-plugin` 可以幫助我們將上一次的 dist 清除,保證無冗餘檔案。 安裝 `clean-webpack-plugin`: ```bash npm i clean-webpack-plugin -D ``` 配置 plugins: ```js const CleanWebpackPlugin = require('clean-webpack-plugin') module.exports = { plugins: [ new CleanWebpackPlugin() ] } ``` 外掛會預設清除 `output.path` 的資料夾。 ### 自定義壓縮選項 webpack 從 v4.26.0 開始內建的壓縮外掛變為 `terser-webpack-plugin`。如果沒有其他需求,自定義壓縮外掛也儘量保持與官方的一致。 安裝 `terser-webpack-plugin`: ```bash npm i terser-webpack-plugin -D ``` 配置 plugins: ```js const TerserPlugin = reuqire('terser-webpack-plugin') module.exports = { optimization: { // 預設為 true minimize: true, minimizer: [ new TerserPlugin() ] }, } ``` 外掛壓縮配置可以查閱 [terser-webpack-plugin 文件](https://www.npmjs.com/package/terser-webpack-plugin),在呼叫時自定義選項。 像上面的 css 壓縮外掛也可以新增到 `optimization.minimizer`。與配置到 plugins 的區別是,配置到 plugins 的外掛在任何情況都會去執行,而配置到 `minimizer` 中,只有在 `minimize` 屬性開啟時才會工作。 這樣做的目的便於通過 `minimize` 屬性來統一控制壓縮。 ```js const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin') const TerserPlugin = reuqire('terser-webpack-plugin') module.exports = { optimization: { // 預設為 true minimize: true, minimizer: [ new TerserPlugin(), new OptimizeCssAssetsWebpackPlugin() ] }, } ``` 注意如果你提供 `minimizer` 選項而沒有使用 js 壓縮外掛,即使 webpack 內建 js 壓縮,打包出來的 js 也不會被壓縮。因為 webpack 壓縮配置會被 `minimizer` 覆蓋。 ## 排查錯誤的建議 在使用 webpack 的過程中,這玩意偶爾會有些奇奇怪怪的報錯。 下面是我遇到的一些錯誤以及解決方法(僅供參考並不是萬能法則): 1. 一些 loader 和 plugin 在使用時,會依賴 webpack 的版本。如果使用過程發生錯誤,檢查是否有版本不相容的問題,可以嘗試降一個版本。 2. 重新安裝依賴,有可能下載過程中,一些依賴會沒裝上。 3. 檢視使用文件,不同版本所傳入的選項屬性可能會不一樣(被坑過) 。 還有注意控制檯的提示,一般根據錯誤提示都能猜出大概是什麼問題。 ## 依賴版本和完整配置 專案結構: ![](https://s3.ax1x.com/2020/12/15/rMC9AS.jpg) 依賴版本: ```json { "@babel/core": "^7.12.3", "@babel/preset-env": "^7.12.1", "autoprefixer": "^10.0.1", "babel-loader": "^8.1.0", "clean-webpack-plugin": "^3.0.0", "copy-webpack-plugin": "^6.3.0", "cross-env": "^7.0.2", "css-loader": "^5.0.0", "file-loader": "^6.2.0", "html-loader": "^1.3.2", "html-webpack-plugin": "^4.5.0", "mini-css-extract-plugin": "^0.9.0", "postcss": "^8.1.6", "postcss-loader": "^4.0.4", "style-loader": "^2.0.0", "url-loader": "^4.1.1", "webpack": "^4.44.2", "webpack-cli": "^4.2.0", "webpack-dev-server": "^3.11.0" } ``` webpack.config.js: ```js const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') const CopyPlugin = require('copy-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin') const { CleanWebpackPlugin } = require('clean-webpack-plugin') /** @type {import('webpack').Configuration} */ module.exports = { devtool: process.env.NODE_ENV === 'development' ? 'source-map' : 'none', entry: { main: './src/js/main.js' }, output: { filename: '[name].[hash].js', path: path.join(__dirname, 'dist') }, devServer: { contentBase: path.join(__dirname, 'dist'), port: 8000, compress: true, open: true, }, plugins: [ // 清除上一次的打包內容 new CleanWebpackPlugin(), // 複製檔案到dist new CopyPlugin({ patterns: [ { from: 'src/music', to: 'music', } ] }), // 使用html模版 new HtmlWebpackPlugin({ template: './src/index.html', minify: true }), // 分離css new MiniCssExtractPlugin({ filename: 'css/[name].[hash].css', chunkFilename: 'css/[id].[hash].css' }), // 壓縮css new OptimizeCssAssetsWebpackPlugin() ], module: { rules: [ // 解析js(es6轉es5) { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }, { test: /\.css$/, use: [ 'style-loader', { loader: MiniCssExtractPlugin.loader, options: { publicPath: '../', } }, 'css-loader', 'postcss-loader' ] }, { test: /\.(html)$/, use: [{ // 主要為了解析html中的img圖片路徑 需要配合url-loader或file-loader使用 loader: 'html-loader', options: { attributes: { list: [{ tag: 'img', attribute: 'src', type: 'src', },{ tag: 'source', attribute: 'src', type: 'src', }] }, minimize: true } }] }, // 解析圖片 { test: /\.(jpg|png|gif|svg)$/, use: [{ loader: 'url-loader', // 小於50k的圖片轉為base64,超出的打包到images資料夾 options: { limit: 1024 * 50, outputPath: './images/', pulbicPath: './images/' } }] }, // 解析其他型別檔案(如字型) { test: /\.(eot|ttf)$/, use: ['file-loader'] }, { test: /\.(mp3)$/, use: [{ loader: 'file-loader', options: { outputPath: './music/', pulbicPath: './music/' } }] } ] }, } ``` ## 最後 由於單頁專案簡單,配置項比較樸實無華,本文主要是些基礎配置。不過套路都差不多,根據專案的需求去選擇 loader 和 plugin。更多的還是要了解這些外掛的作用和使用方法,以及其他常用的