源代碼css
熟悉 webpack 與 webpack4 配置。html
webpack4 相對於 3 的最主要的區別是所謂的零配置
,可是爲了知足咱們的項目需求仍是要本身進行配置,不過咱們可使用一些 webpack 的預設值。同時 webpack 也拆成了兩部分,webpack 和 webpack-cli,都須要本地安裝。 vue
咱們經過實現一個 vue 的開發模板(vue init webpack 模板,其實跟 vue 關係不太大)來進行一次體驗。在配置過程當中會盡可能使用 webpack4 的相關內容。node
本文不作 webpack 配置的完整介紹,着重介紹配置過程當中須要注意的地方。查看代碼註釋閱讀效果更佳,完整配置與詳細註釋可見源代碼。配置位於 build 文件夾下。webpack
與版本 4 相關的章節會添加符號 ④。ios
須要注意的一點是,咱們的 webpack 代碼是運行在node環境下的,這部分代碼可使用 node api,可是咱們的業務代碼(src下)是沒法使用 node api 的。git
因爲 webpack 配置中的如 context,entry(chunk入口),output(輸出)和 module.rules 中 loaders 的配置在開發模式和生產模式基本都是公用的,因此咱們提取到 webpack.base.js
文件內,供複用。其中 output 部分以下:github
output: { path: path.resolve(__dirname, '../dist/'), // 資源文件輸出時寫入的路徑 filename: 'static/js/[name].[chunkhash].js', // 使用 chunkhash 加入文件名作文件更新和緩存處理 chunkFilename: 'static/js/[name].[chunkhash].js' }
須要注意的有:web
hash 是用在文件輸出的名字中的,如 [name].[hash].js
,總的來講,webpack 提供了三種 hash:vue-router
[hash]
:這次打包的全部內容的 hash。[chunkhash]
:每個 chunk 都根據自身的內容計算而來。[contenthash]
:由 css 提取插件提供,根據自身內容計算得來。三種 hash 的使用,咱們在優化部分再講,先優先使用 [chunkhash]
。
loader 優先級須要注意兩點,
同 test 配置內優先級:在同一個 test 下配置多個 loader ,優先處理的 loader 放在配置數組的後面,如對 less 處理,則:
{ test: /\.less$/, use: [ 'style-loader', 'css-loader', 'postcss-loader', 'less-loader' ] }
不一樣 test 內優先級:如對 js 文件的處理須要兩個 test 分別配置,使用 eslint-loader
和 babel-loader
,可是又不能配置在一個配置對象內,可以使用 enforce: 'pre' 強調優先級,由 eslint-loader
優先處理。
{ test: /\.(js|vue)$/, loader: 'eslint-loader', enforce: 'pre', }, { test: /\.js$/, loader: 'babel-loader' }
咱們以 less 文件的 loader 配置 ['vue-style-loader', 'css-loader', 'postcss-loader', 'less-loader']
,使用 @import url(demo.less)
爲例:
vue-style-loader 功能相似 style-loader
可是因爲 vue 中的單文件組件,又分爲兩種狀況:
.vue 文件內的 style: vue-loader
會對 .vue 單文件組件進行處理,對 .vue 單文件組件內的各類 lang="type" 咱們能夠在 vue-loader
的 options 配置不一樣的 loader,因爲 vue-loader
內置了 postcss
對 css 進行處理,因此此處咱們不須要再配置 postcss-loader
{ test: /\.vue$/, loader: 'vue-loader', options: { loaders: { less: ['// xxx-loaders'], scss: ['// xxx-loaders'], } } }
import 'demo.less'
,這種方式引入的樣式文件,在 vue-loader
處理範圍置以外,因此仍然須要配置 postcss-loader
。因爲這種差別咱們將 對 css 預處理器文件的配置封裝爲函數,由 usePostCss
參數生成對應配置,將文件放入 utils.js
文件內,將 vue-loader
配置放在 vue-loader.js
文件內。
也就是對 css 預處理器的配置咱們須要在 vue-loader
內和 webpack
內配置兩遍。
寫這篇 README.md 期間 vue-loader 發佈了 v15 版,須要配合插件使用,不用再進行兩遍配置
postcss-loader 是一個強大的 css 處理工具,咱們將 postcss 的配置拆分出去,新建 postcss.config.js
配置文件
module.exports = { plugins: { // 處理 @import 'postcss-import': {}, // 處理 css 中 url 'postcss-url': {}, // 自動前綴 'autoprefixer': { "browsers": [ "> 1%", "last 2 versions" ] } } }
除了註釋中列出的須要的功能插件,咱們還可能會用到 nextcss
(新的css語法的處理),px2rem/px-to-viewport
移動端適配相關的插件。
咱們使用 babel 編譯瀏覽器不能識別的 js、類 js 語法,如轉義 ES6+、JSX等。一樣將 babel-loader 的配置拆分出去,須要建立 .babelrc
並配置:
{ "presets": [ [ /* * * babel-preset-env * 能夠根據配置的目標運行環境自動啓用須要的 babel 插件。 */ "env", { "modules": false, // 關閉 babel 對 es module 的處理 "targets": { // 目標運行環境 "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] } } ] ], "plugins": [ "syntax-dynamic-import" // 異步加載語法編譯插件 ] }
咱們還須要對圖片、視頻、字體等文件進行 loader 配置,以字體文件爲例子,主要用到的是 url-loader:
{ /** * 末尾 \?.* 匹配帶 ? 資源路徑 * 咱們引入的第三方 css 字體樣式對字體的引用路徑中可能帶查詢字符串的版本信息 */ test: /\.(woff2|woff|eot|ttf|otf)(\?.*)?$/, /** * url-loader * 會配合 webpack 對資源引入路徑進行復寫,如將 css 提取成獨立文件,可能出現 404 錯誤可查看 提取 js 中的 css 部分解決 * 會以 webpack 的輸出路徑爲基本路徑,以 name 配置進行具體輸出 * limit 單位爲 byte,小於這個大小的文件會編譯爲 base64 寫進 js 或 html */ loader: 'url-loader', options: { limit: 10000, name: 'static/fonts/[name].[hash:7].[ext]', } }
直接引用(絕對路徑)和代碼執行時肯定的資源路徑應該是以靜態文件存在的,這些資源文件不會通過 webpack 編譯處理,因此咱們將它們放在獨立的文件夾(如 static)中,並在代碼打包後拷貝到咱們的輸出目錄,咱們使用 copy-webpack-plugin 自動完成這個工做:
const CopyWebpackPlugin = require('copy-webpack-plugin') // 在開發模式下,會將文件寫入內存 new CopyWebpackPlugin([ { from: path.resolve(__dirname, '../static'), to: 'static', ignore: ['.*'] } ])
此插件在拷貝文件過多時會崩潰,不知道解決了沒有。
咱們先進行生產模式的配置。
在 package.json 下添加
"scripts": { "build": "node build/build.js"` }
那麼使用 npm run build
命令就可執行 node build/build.js
,咱們不直接使用 webpack webpack.prod.config.js
命令去執行配置文件,而是在 build.js 中,作一些文件刪除的處理,再啓動 webpack。
主要是兩個工做,引入 rimraf
模塊刪除 webpack 下以前產生的指定文件,啓動 webpack,並在不一樣階段給出不一樣的提示信息。
// 在第一行設置當前爲 生產環境 process.env.NODE_ENV = 'production' const webpack = require('webpack') const rm = require('rimraf') const webpackConfig = require('./webpack.prod') // 刪除 webpack 輸出目錄下的內容,也可只刪除子文件如 static 等 rm(webpackConfig.output.path, err => { // webpack 按照生產模式配置啓動 webpack(webpackConfig, (err, stats) => { // 輸出一些狀態信息 }) }
更多細節見源代碼註釋。
新建 webpack.prod.js
文件,使用
const merge = require('webpack-merge') // 專用合併 webpack 配置的包 const webpackBaseConfig = require('./webpack.base') module.exports = merge(webpackBaseConfig, { // 生產模式配置 })
合併基本配置和生產模式獨有配置,而後咱們開始進行生產模式下的 webpack 的配置信息的填寫。
這是 webpack4 的新 api ,有三個預設值:development
,production
,none
,咱們在生產模式選用mode: 'production'
,webpack4在此配置下默認啓用了:
插件
因此這些默認啓用的內容咱們不須要再配置。
最後一點設置 process.env.NODE_ENV 的值設爲 production
實際上是使用 DefinePlugin 插件:
new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") })
從而咱們能夠在業務代碼中經過 process.env.NODE_ENV
,如進行判斷,使用開發接口仍是線上接口。若是咱們須要在 webpack 中判斷當前環境,還須要單獨的設置 process.env.NODE_ENV = 'production'
,這也是咱們在 build.js
中第一行作的事情。
<script src="./bundles.js"></script>
(還可能包括後面提取出來的 css 文件)到 HTML 文件。const HtmlWebpackPlugin = require('html-webpack-plugin') plugins: [ new HtmlWebpackPlugin({ filename: path.join(__dirname, '../dist/index.html'),// 文件寫入路徑 template: path.join(__dirname, '../src/index.html'),// 模板文件路徑 inject: true // js 等 bundles 插入 html 的位置 head/body等 }) ]
若是不對 HtmlWebpackPlugin 進行配置,則其會建立一個 HTML 文件,其中 filename
在開發模式下仍是比較重要的。
使用過 webpack3 的同窗應該對 extract-text-webpack-plugin 插件(以舊插件代稱)比較熟悉,爲了嘗試webpack4,我並不想使用這個插件的 @next
版本,因此選擇了新的替代插件 mini-css-extract-plugin(以新插件代稱)。
與舊插件相同,一樣須要在 webpack 的 loader 部分和 plugin 部分都進行配置,不一樣的是新插件提供了單獨的 loader,在 loader 部分與舊插件的配置方式不太相同。配置以下:
loader 部分
const MiniCssExtractPlugin = require("mini-css-extract-plugin") // [ { loader: MiniCssExtractPlugin.loader, options: { // (segmentfault 這兒的多行註釋渲染有點問題 😰,改爲單行註釋形式) // 複寫 css 文件中資源路徑 // webpack3.x 配置在 extract-text-webpack-plugin 插件中 // 由於 css 文件中的外鏈是相對與 css 的, // 咱們抽離的 css 文件在可能會單獨放在 css 文件夾內 // 引用其餘如 img/a.png 會尋址錯誤 // 這種狀況下因此單獨須要配置 publicPath,複寫其中資源的路徑 // publicPath: '../' } }, { loader: 'css-loader', options: {} }, { loader: 'less-loader', options: {} } ]
plugin 部分
new MiniCssExtractPlugin({ // 輸出到單獨的 css 文件夾下 filename: "static/css/[name].[chunkhash].css" })
能夠看到這個 loader 也配置在了 css 預處理器部分,在前面咱們已經把 css 預處理器的配置提取到了 utils.js 文件的函數內,因此這裏也是,咱們使用 extract
參數決定是否須要提取。
回憶一下,以前使用的 style-loader
或 vue-style-loader
的做用,它們會建立標籤將 css 的內容直接插入到 HTML中。而提取成獨立的 css 文件以後,插入到 HTML 的工做由 html-webpack-plugin
插件完成,二者職責的這部分職責是重複的,因此咱們須要使用 extract
參數作相似以下處理:
if (options.extract) { return [MiniCssExtractPlugin.loader, ...otherLoaders] } else { return ['vue-style-loader', ...otherLoaders] }
這是 webpack 配置中很重要的一個環節,影響到咱們使用瀏覽器緩存的合理性,影響頁面資源的加載速度,將 js 進行合理拆分,能夠有效減少咱們每次更新代碼影響到的文件範圍。
使用過 webpack3 的同窗必定清楚,咱們通常會提取出這麼幾個文件 manifest.js
(webpack 運行時,即webpack解析其餘bundle的代碼等)、vendor.js
(node_modules內的庫)、app.js(真正的項目業務代碼)。在 webpack3 中咱們使用 webpack.optimize.CommonsChunkPlugin
插件進行提取,webpack4 中咱們能夠直接使用 optimization 配置項進行配置(固然仍可以使用插件配置):
/** * 優化部分包括代碼拆分 * 且運行時(manifest)的代碼拆分提取爲了獨立的 runtimeChunk 配置 */ optimization: { splitChunks: { chunks: "all", cacheGroups: { // 提取 node_modules 中代碼 vendors: { test: /[\\/]node_modules[\\/]/, name: "vendors", chunks: "all" }, commons: { // async 設置提取異步代碼中的公用代碼 chunks: "async" name: 'commons-async', /** * minSize 默認爲 30000 * 想要使代碼拆分真的按照咱們的設置來 * 須要減少 minSize */ minSize: 0, // 至少爲兩個 chunks 的公用代碼 minChunks: 2 } } }, /** * 對應原來的 minchunks: Infinity * 提取 webpack 運行時代碼 * 直接置爲 true 或設置 name */ runtimeChunk: { name: 'manifest' } }
也可將不會變的開發依賴配置到單獨的entry中,如:
entry: { app: 'index.js', vendor2: ['vue', 'vue-router', 'axios'] }
開發模式與生產模式的不一樣是,在開發時會頻繁運行代碼,因此不少東西在開發模式是不推薦配置的,如css文件提取,代碼壓縮等。因此針對一些寫入公共配置文件,可是開發模式不須要的功能,咱們須要作相似修改:process.env.NODE_ENV === 'production' ? true : false
,如 css 預處理中是否須要配置提取 loader MiniCssExtractPlugin.loader
。此外還有一些是隻配置在生產模式下的,如 MiniCssExtractPlugin
和 js 代碼拆分優化。
開發模式咱們須要一個開發服務,幫咱們完成實時更新、接口代理等功能。咱們使用 webpack-dev-server
。須要 npm 安裝。
一樣,在 package.json 下添加
"scripts": { "dev": "webpack-dev-server --config ./build/webpack.dev.js" }
使用 --config
指定配置文件,因爲命令直接調用 webpack-dev-server 運行,因此咱們直接寫配置就好,能夠不像生產模式同樣去編寫調用邏輯。
新建 webpack.dev.js
文件,一樣使用:
// 在第一行設置當前環境爲開發環境 process.env.NODE_ENV = 'development' const merge = require('webpack-merge') // 專用合併webpack配置的包 const webpackBaseConfig = require('./webpack.base') module.exports = merge(webpackBaseConfig, { // 開發模式配置 })
一樣,在開發模式下咱們能夠將 mode
配置爲 development
,一樣默認啓用了一些功能:
插件
devServer: { clientLogLevel: 'warning', inline: true, // 啓動熱更新 hot: true, // 在頁面上全屏輸出報錯信息 overlay: { warnings: true, errors: true }, // 顯示 webpack 構建進度 progress: true, // dev-server 服務路徑 contentBase: false, compress: true, host: 'localhost', port: '8080', // 自動打開瀏覽器 open: true, // 能夠進行接口代理配置 proxy: xxx, // 跟 friendly-errors-webpack-plugin 插件配合 quiet: true, publicPath: '/' }
devServer 使用熱更新 hot 時須要使用插件:
plugins: [ new webpack.HotModuleReplacementPlugin() ]
優化 webpack 輸出信息,須要配置:
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') plugins: [ new FriendlyErrorsPlugin() ]
熱更新:在使用熱更新時,咱們的 chunk 名中不能使用 [hash]
作標識,文件名變化沒法熱更新,因此須要將原來配置在公共配置中的 output 中的文件名配置分別寫入生產和開發模式配置中,開發模式去掉 [hash]
filename: 'static/[name].js', chunkFilename: 'static/[id].js'
HtmlWebpackPlugin:在生產模式下,咱們將 html 文件寫入到 dist 下,可是在開發模式下,並無實際的寫入過程,且 devServer
啓動後的服務內容與 contentBase
有關,二者須要一致,因此咱們將 HtmlWebpackPlugin
的配置也分爲 生產和開發模式,開發模式下使用:
new HtmlWebpackPlugin({ filename: 'index.html', // 文件寫入路徑,前面的路徑與 devServer 中 contentBase 對應 template: path.resolve(__dirname, '../src/index.html'),// 模板文件路徑 inject: true })
咱們能夠提取到獨立的 config 文件中(本代碼沒作)。
在生產模式的 拆分 js 代碼
部分咱們已經講了如何拆分,那麼爲了更好的分析咱們的拆分是否合理,咱們能夠配置一個 bundle 組成分析的插件。
const BundleAnalyzer = require('webpack-bundle-analyzer') plugins: [ new BundleAnalyzer.BundleAnalyzerPlugin() ]
咱們使用文件名中的 hash 變化來進行資源文件的更新,那麼合理利用緩存時,就要求咱們合理的拆分文件,在內容更新時最小限度的影響文件名中的 hash。這裏就用到了[hash]
,[chunkhash]
,[contenthash]
。然而 webpack 對 hash 的默認處理並不盡如人意,這一部分的優化能夠參考基於 webpack 的持久化緩存方案
多頁面配置代碼位於 muilt-pages 分支。咱們只需作少許修改,以目前有 entry 頁和 index 頁爲例。
將兩個頁面的 js 入口都配置在 webpack
的 entry
中:
entry: { /** * 入口,chunkname: 路徑 * 多入口可配置多個 */ main: './src/main.js', entry: './src/entry.js' }
也能夠本身設置項目結構,使用 node api 動態讀取的方式獲取目前的多頁面入口。
需按照頁面個數配置多個 HtmlWebpackPlugin
:
new HtmlWebpackPlugin({ filename: path.join(__dirname, '../dist/main.html'),// 文件寫入路徑 template: path.join(__dirname, '../src/index.html'),// 模板文件路徑 inject: true, // 插入位置 chunks: ['manifest', 'vendors', 'common', 'main'] }), new HtmlWebpackPlugin({ filename: path.join(__dirname, '../dist/entry.html'),// 文件寫入路徑 template: path.join(__dirname, '../src/index.html'),// 模板文件路徑 inject: true, // 插入位置 chunks: ['manifest', 'vendors', 'common', 'entry'] }),
其中需手動指定每一個頁面的插入的 chunks(同步的),不然會將其餘頁面的文件也一同插入當前頁面。
在單頁面下,通常不存在提取非異步 js 文件的公共代碼(非 node_modules)的問題,在多頁面下咱們的頁面間可能會公用 api、配置等文件,此時能夠增長:
'common': { // initial 設置提取同步代碼中的公用代碼 chunks: 'initial', // test: 'xxxx', 也可以使用 test 選擇提取哪些 chunks 裏的代碼 name: 'common', minSize: 0, minChunks: 2 }
提取同步代碼中的公用代碼