相信vue使用者對vue-cli
都不會陌生,甚至可以說,很熟悉了,但對其webpack
的配置可能知之甚少吧。
過完年回來後,我接手了公司的新專案。新專案是一個spa。很自然,我就想到了vue-cli腳手架了,當時研究一下它的webpack配置。於是,就有了其他的內容。
今天這篇文章,是在原來的基礎上,增加了一些新版本的內容,但實質上變化不大。
說明
此倉庫為vue-cli webpack
的配置分析,其實只是在原始碼中加上註釋而已。大家檢視詳細分析,可以從後面提到的入口檔案開始檢視。
分析不包括check-versions.js
檔案,因為check-versions.js
是檢測npm
和node
版本,不涉及webpack
,所以就沒有對check-versions.js
進行分析。同時,也不包括測試部分的程式碼,該分析只是針對開發和生產環境的webpack
配置進行分析。
vue-cli 版本
2.8.1
入口
從package.json
可以看到開發和生產環境的入口。
"scripts": {
"dev": "node build/dev-server.js",
"build": "node build/build.js"
}
開發環境
開發環境的入口檔案是 build/dev-server.js。
dev-server.js
該檔案中,使用express作為後端框架,結合一些關於webpack的中介軟體,搭建了一個開發環境。
// 配置檔案
var config = require(`../config`)
// 如果 Node 的環境無法判斷當前是 dev / product 環境
// 使用 config.dev.env.NODE_ENV 作為當前的環境
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
// 可以強制開啟瀏覽器並跳轉到指定 url 的外掛
// https://github.com/sindresorhus/opn
var opn = require(`opn`)
// node自帶的檔案路徑工具
var path = require(`path`)
// express框架
var express = require(`express`)
var webpack = require(`webpack`)
// 測試環境,使用的配置與生產環境的配置一樣
// 非測試環境,即為開發環境,因為此檔案只有測試環境和開發環境使用
var proxyMiddleware = require(`http-proxy-middleware`)
var webpackConfig = process.env.NODE_ENV === `testing`
// 生產環境配置檔案
? require(`./webpack.prod.conf`)
// 開發環境配置檔案
: require(`./webpack.dev.conf`)
// 埠號為命令列輸入的PORT引數或者配置檔案中的預設值
var port = process.env.PORT || config.dev.port
// 配置檔案中 是否自動開啟瀏覽器
var autoOpenBrowser = !!config.dev.autoOpenBrowser
// 配置檔案中 http代理配置
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable
// 啟動 express 服務
var app = express()
// 啟動 webpack 編譯
var compiler = webpack(webpackConfig)
// 可以將編譯後的檔案暫存到記憶體中的外掛
// https://github.com/webpack/webpack-dev-middleware
var devMiddleware = require(`webpack-dev-middleware`)(compiler, {
// 公共路徑,與webpack的publicPath一樣
publicPath: webpackConfig.output.publicPath,
// 不列印
quiet: true
})
// Hot-reload 熱過載外掛
// https://github.com/glenjamin/webpack-hot-middleware
var hotMiddleware = require(`webpack-hot-middleware`)(compiler, {
log: () => {}
})
// 當tml-webpack-plugin template更改之後,強制重新整理瀏覽器
compiler.plugin(`compilation`, function (compilation) {
compilation.plugin(`html-webpack-plugin-after-emit`, function (data, cb) {
hotMiddleware.publish({ action: `reload` })
cb()
})
})
// 將 proxyTable 中的請求配置掛在到啟動的 express 服務上
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
// 如果options的資料型別為string,則表示只設定了url,
// 所以需要將url設定為物件中的 target的值
if (typeof options === `string`) {
options = { target: options }
}
app.use(proxyMiddleware(options.filter || context, options))
})
// 使用 connect-history-api-fallback 匹配資源
// 如果不匹配就可以重定向到指定地址
// https://github.com/bripkens/connect-history-api-fallback
app.use(require(`connect-history-api-fallback`)())
// 將暫存到記憶體中的 webpack 編譯後的檔案掛在到 express 服務上
app.use(devMiddleware)
// 將 Hot-reload 掛在到 express 服務上
app.use(hotMiddleware)
// 拼接 static 資料夾的靜態資源路徑
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
// 靜態檔案服務
app.use(staticPath, express.static(`./static`))
var uri = `http://localhost:` + port
// 編譯成功後列印網址資訊
devMiddleware.waitUntilValid(function () {
console.log(`> Listening at ` + uri + `
`)
})
module.exports = app.listen(port, function (err) {
if (err) {
console.log(err)
return
}
// 如果配置了自動開啟瀏覽器,且不是測試環境,則自動開啟瀏覽器並跳到我們的開發地址
if (autoOpenBrowser && process.env.NODE_ENV !== `testing`) {
opn(uri)
}
})
webpack.dev.conf.js
dev-server.js
中使用了webpack.dev.conf.js
檔案,該檔案是開發環境中webpack的配置入口。
// 工具函式集合
var utils = require(`./utils`)
var webpack = require(`webpack`)
// 配置檔案
var config = require(`../config`)
// webpack 配置合併外掛
var merge = require(`webpack-merge`)
// webpac基本配置
var baseWebpackConfig = require(`./webpack.base.conf`)
// 自動生成 html 並且注入到 .html 檔案中的外掛
// https://github.com/ampedandwired/html-webpack-plugin
var HtmlWebpackPlugin = require(`html-webpack-plugin`)
// webpack錯誤資訊提示外掛
// https://github.com/geowarin/friendly-errors-webpack-plugin
var FriendlyErrorsPlugin = require(`friendly-errors-webpack-plugin`)
// 將 Hol-reload 熱過載的客戶端程式碼新增到 webpack.base.conf 的 對應 entry 中,一起打包
Object.keys(baseWebpackConfig.entry).forEach(function(name) {
baseWebpackConfig.entry[name] = [`./build/dev-client`].concat(baseWebpackConfig.entry[name])
})
module.exports = merge(baseWebpackConfig, {
module: {
// styleLoaders
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
},
// 最新的配置為 cheap-module-eval-source-map,雖然 cheap-module-eval-source-map更快,但它的定位不準確
// 所以,換成 eval-source-map
devtool: `#eval-source-map`,
plugins: [
// definePlugin 接收字串插入到程式碼當中, 所以你需要的話可以寫上 JS 的字串
// 此處,插入適當的環境
// https://webpack.js.org/plugins/define-plugin/
new webpack.DefinePlugin({
`process.env`: config.dev.env
}),
// HotModule 外掛在頁面進行變更的時候只會重繪對應的頁面模組,不會重繪整個 html 檔案
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
// 將 index.html 作為入口,注入 html 程式碼後生成 index.html檔案
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: `index.html`,
template: `index.html`,
inject: true
}),
// webpack錯誤資訊提示外掛
new FriendlyErrorsPlugin()
]
})
webpack.base.conf.js
在webpack.dev.conf.js
中出現webpack.base.conf.js
,這個檔案是開發環境和生產環境,甚至測試環境,這些環境的公共webpack配置。可以說,這個檔案相當重要。
// node自帶的檔案路徑工具
var path = require(`path`)
// 工具函式集合
var utils = require(`./utils`)
// 配置檔案
var config = require(`../config`)
// 工具函式集合
var vueLoaderConfig = require(`./vue-loader.conf`)
/**
* 獲得絕對路徑
* @method resolve
* @param {String} dir 相對於本檔案的路徑
* @return {String} 絕對路徑
*/
function resolve(dir) {
return path.join(__dirname, `..`, dir)
}
module.exports = {
entry: {
app: `./src/main.js`
},
output: {
// 編譯輸出的靜態資源根路徑
path: config.build.assetsRoot,
// 編譯輸出的檔名
filename: `[name].js`,
// 正式釋出環境下編譯輸出的上線路徑的根路徑
publicPath: process.env.NODE_ENV === `production` ?
config.build.assetsPublicPath : config.dev.assetsPublicPath
},
resolve: {
// 自動補全的副檔名
extensions: [`.js`, `.vue`, `.json`],
// 路徑別名
alias: {
// 例如 import Vue from `vue`,會自動到 `vue/dist/vue.common.js`中尋找
`vue$`: `vue/dist/vue.esm.js`,
`@`: resolve(`src`),
}
},
module: {
rules: [{
// 審查 js 和 vue 檔案
// https://github.com/MoOx/eslint-loader
test: /.(js|vue)$/,
loader: `eslint-loader`,
// 表示預先處理
enforce: "pre",
include: [resolve(`src`), resolve(`test`)],
options: {
formatter: require(`eslint-friendly-formatter`)
}
},
{
// 處理 vue檔案
// https://github.com/vuejs/vue-loader
test: /.vue$/,
loader: `vue-loader`,
options: vueLoaderConfig
},
{
// 編譯 js
// https://github.com/babel/babel-loader
test: /.js$/,
loader: `babel-loader`,
include: [resolve(`src`), resolve(`test`)]
},
{
// 處理圖片檔案
// https://github.com/webpack-contrib/url-loader
test: /.(png|jpe?g|gif|svg)(?.*)?$/,
loader: `url-loader`,
query: {
limit: 10000,
name: utils.assetsPath(`img/[name].[hash:7].[ext]`)
}
},
{
// 處理字型檔案
test: /.(woff2?|eot|ttf|otf)(?.*)?$/,
loader: `url-loader`,
query: {
limit: 10000,
name: utils.assetsPath(`fonts/[name].[hash:7].[ext]`)
}
}
]
}
}
config/index.js
該檔案在很多檔案中都用到,是主要的配置檔案,包含靜態檔案的路徑、是否開啟sourceMap等。其中,分為兩個部分dev
(開發環境的配置)和build
(生產環境的配置)。
// 詳情見文件:https://vuejs-templates.github.io/webpack/env.html
var path = require(`path`)
module.exports = {
// production 生產環境
build: {
// 構建環境
env: require(`./prod.env`),
// 構建輸出的index.html檔案
index: path.resolve(__dirname, `../dist/index.html`),
// 構建輸出的靜態資源路徑
assetsRoot: path.resolve(__dirname, `../dist`),
// 構建輸出的二級目錄
assetsSubDirectory: `static`,
// 構建釋出的根目錄,可配置為資源伺服器域名或 CDN 域名
assetsPublicPath: `/`,
// 是否開啟 cssSourceMap
productionSourceMap: true,
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
// 預設關閉 gzip,因為很多流行的靜態資源主機,例如 Surge、Netlify,已經為所有靜態資源開啟gzip
productionGzip: false,
// 需要使用 gzip 壓縮的副檔名
productionGzipExtensions: [`js`, `css`],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
// 執行“build”命令列時,加上一個引數,可以在構建完成後參看包分析報告
// true為開啟,false為關閉
bundleAnalyzerReport: process.env.npm_config_report
},
// dev 開發環境
dev: {
// 構建環境
env: require(`./dev.env`),
// 埠號
port: 3333,
// 是否自動開啟瀏覽器
autoOpenBrowser: true,
assetsSubDirectory: `static`,
// 編譯釋出的根目錄,可配置為資源伺服器域名或 CDN 域名
assetsPublicPath: `/`,
// proxyTable 代理的介面(可跨域)
// 使用方法:https://vuejs-templates.github.io/webpack/proxy.html
proxyTable: {},
// CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README
// (https://github.com/webpack/css-loader#sourcemaps)
// In our experience, they generally work as expected,
// just be aware of this issue when enabling this option.
// 預設情況下,關閉 CSS Sourcemaps,因為使用相對路徑會報錯。
// CSS-Loader README:https://github.com/webpack/css-loader#sourcemaps
cssSourceMap: false
}
}
utils.js
utils.js
也是一個被使用頻率的檔案,這個檔案包含了三個工具函式:
-
生成靜態資源的路徑
-
生成 ExtractTextPlugin物件或loader字串
-
生成 style-loader的配置
// node自帶的檔案路徑工具
var path = require(`path`)
// 配置檔案
var config = require(`../config`)
// 提取css的外掛
// https://github.com/webpack-contrib/extract-text-webpack-plugin
var ExtractTextPlugin = require(`extract-text-webpack-plugin`)
/**
* 生成靜態資源的路徑
* @method assertsPath
* @param {String} _path 相對於靜態資原始檔夾的檔案路徑
* @return {String} 靜態資源完整路徑
*/
exports.assetsPath = function (_path) {
var assetsSubDirectory = process.env.NODE_ENV === `production`
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
// path.posix.join與path.join一樣,不過總是以 posix 相容的方式互動
return path.posix.join(assetsSubDirectory, _path)
}
/**
* 生成處理css的loaders配置
* @method cssLoaders
* @param {Object} options 生成配置
* option = {
* // 是否開啟 sourceMap
* sourceMap: true,
* // 是否提取css
* extract: true
* }
* @return {Object} 處理css的loaders配置物件
*/
exports.cssLoaders = function (options) {
options = options || {}
var cssLoader = {
loader: `css-loader`,
options: {
minimize: process.env.NODE_ENV === `production`,
sourceMap: options.sourceMap
}
}
/**
* 生成 ExtractTextPlugin物件或loader字串
* @method generateLoaders
* @param {Array} loaders loader名稱陣列
* @return {String|Object} ExtractTextPlugin物件或loader字串
*/
function generateLoaders (loader, loaderOptions) {
var loaders = [cssLoader]
if (loader) {
loaders.push({
// 例如,sass?indentedSyntax
// 在?號前加上“-loader”
loader: loader + `-loader`,
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// extract為true時,提取css
// 生產環境中,預設為true
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: `vue-style-loader`
})
} else {
return [`vue-style-loader`].concat(loaders)
}
}
// http://vuejs.github.io/vue-loader/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders(`less`),
sass: generateLoaders(`sass`, { indentedSyntax: true }),
scss: generateLoaders(`sass`),
stylus: generateLoaders(`stylus`),
styl: generateLoaders(`stylus`)
}
}
/**
* 生成 style-loader的配置
* style-loader文件:https://github.com/webpack/style-loader
* @method styleLoaders
* @param {Object} options 生成配置
* option = {
* // 是否開啟 sourceMap
* sourceMap: true,
* // 是否提取css
* extract: true
* }
* @return {Array} style-loader的配置
*/
exports.styleLoaders = function (options) {
var output = []
var loaders = exports.cssLoaders(options)
for (var extension in loaders) {
var loader = loaders[extension]
output.push({
test: new RegExp(`\.` + extension + `$`),
use: loader
})
}
return output
}
生產環境
開發環境的入口檔案是build/build.js
。
build.js
該檔案,為構建打包檔案,會將原始碼進行構建(編譯、壓縮等)後打包。
// 設定當前環境為生產環境
process.env.NODE_ENV = `production`
// loading 外掛
// https://github.com/sindresorhus/ora
var ora = require(`ora`)
// 可以在 node 中執行`rm -rf`的工具
// https://github.com/isaacs/rimraf
var rm = require(`rimraf`)
// node自帶的檔案路徑工具
var path = require(`path`)
// 在終端輸出帶顏色的文字
// https://github.com/chalk/chalk
var chalk = require(`chalk`)
var webpack = require(`webpack`)
// 配置檔案
var config = require(`../config`)
var webpackConfig = require(`./webpack.prod.conf`)
// 在終端顯示loading效果,並輸出提示
var spinner = ora(`building for production...`)
spinner.start()
// 刪除這個資料夾 (遞迴刪除)
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
// 構建
webpack(webpackConfig, function (err, stats) {
// 構建成功
// 停止 loading動畫
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + `
`)
// 列印提示
console.log(chalk.cyan(` Build complete.
`))
console.log(chalk.yellow(
` Tip: built files are meant to be served over an HTTP server.
` +
` Opening index.html over file:// won`t work.
`
))
})
})
webpack.prod.conf
該檔案,為生產環境中webpack的配置入口。同時,它也依賴於前面提到的webpack.base.conf.js
、utils.js
和config/index.js
。
// node自帶的檔案路徑工具
var path = require(`path`)
// 工具函式集合
var utils = require(`./utils`)
var webpack = require(`webpack`)
// 配置檔案
var config = require(`../config`)
// webpack 配置合併外掛
var merge = require(`webpack-merge`)
// webpack 基本配置
var baseWebpackConfig = require(`./webpack.base.conf`)
// webpack 複製檔案和資料夾的外掛
// https://github.com/kevlened/copy-webpack-plugin
var CopyWebpackPlugin = require(`copy-webpack-plugin`)
// 自動生成 html 並且注入到 .html 檔案中的外掛
// https://github.com/ampedandwired/html-webpack-plugin
var HtmlWebpackPlugin = require(`html-webpack-plugin`)
// 提取css的外掛
// https://github.com/webpack-contrib/extract-text-webpack-plugin
var ExtractTextPlugin = require(`extract-text-webpack-plugin`)
// webpack 優化壓縮和優化 css 的外掛
// https://github.com/NMFR/optimize-css-assets-webpack-plugin
var OptimizeCSSPlugin = require(`optimize-css-assets-webpack-plugin`)
// 如果當前環境為測試環境,則使用測試環境
// 否則,使用生產環境
var env = process.env.NODE_ENV === `testing`
? require(`../config/test.env`)
: config.build.env
var webpackConfig = merge(baseWebpackConfig, {
module: {
// styleLoaders
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true
})
},
// 是否開啟 sourceMap
devtool: config.build.productionSourceMap ? `#source-map` : false,
output: {
// 編譯輸出的靜態資源根路徑
path: config.build.assetsRoot,
// 編譯輸出的檔名
filename: utils.assetsPath(`js/[name].[chunkhash].js`),
// 沒有指定輸出名的檔案輸出的檔名
chunkFilename: utils.assetsPath(`js/[id].[chunkhash].js`)
},
plugins: [
// definePlugin 接收字串插入到程式碼當中, 所以你需要的話可以寫上 JS 的字串
// 此處,插入適當的環境
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
`process.env`: env
}),
// 壓縮 js
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
sourceMap: true
}),
// 提取 css
new ExtractTextPlugin({
filename: utils.assetsPath(`css/[name].[contenthash].css`)
}),
// 壓縮提取出來的 css
// 可以刪除來自不同元件的冗餘程式碼
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin(),
// 將 index.html 作為入口,注入 html 程式碼後生成 index.html檔案
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: process.env.NODE_ENV === `testing`
? `index.html`
: config.build.index,
template: `index.html`,
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// 更多選項 https://github.com/kangax/html-minifier#options-quick-reference
},
// 必須通過 CommonsChunkPlugin一致地處理多個 chunks
chunksSortMode: `dependency`
}),
// 分割公共 js 到獨立的檔案
// https://webpack.js.org/guides/code-splitting-libraries/#commonschunkplugin
new webpack.optimize.CommonsChunkPlugin({
name: `vendor`,
minChunks: function (module, count) {
// node_modules中的任何所需模組都提取到vendor
return (
module.resource &&
/.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, `../node_modules`)
) === 0
)
}
}),
// 將webpack runtime 和模組清單 提取到獨立的檔案,以防止當 app包更新時導致公共 jsd hash也更新
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: `manifest`,
chunks: [`vendor`]
}),
// 複製靜態資源
// https://github.com/kevlened/copy-webpack-plugin
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, `../static`),
to: config.build.assetsSubDirectory,
ignore: [`.*`]
}
])
]
})
// 開啟 gzip 的情況時,給 webpack plugins新增 compression-webpack-plugin 外掛
if (config.build.productionGzip) {
// webpack 壓縮外掛
// https://github.com/webpack-contrib/compression-webpack-plugin
var CompressionWebpackPlugin = require(`compression-webpack-plugin`)
// 向webpackconfig.plugins中加入下方的外掛
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: `[path].gz[query]`,
algorithm: `gzip`,
test: new RegExp(
`\.(` +
config.build.productionGzipExtensions.join(`|`) +
`)$`
),
threshold: 10240,
minRatio: 0.8
})
)
}
// 開啟包分析的情況時, 給 webpack plugins新增 webpack-bundle-analyzer 外掛
if (config.build.bundleAnalyzerReport) {
// https://github.com/th0r/webpack-bundle-analyzer
var BundleAnalyzerPlugin = require(`webpack-bundle-analyzer`).BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig
其他
如果你覺得在segmentfault的程式碼閱讀體驗不好,你可以到我github上將程式碼clone下來看。
總結
這次研究webpack配置的時候,我自己跟著原始碼敲了一遍(很笨的方法),然後,在github和webpack官網上查使用到的外掛的作用和用法。經過這一次折騰,加深對webpack的認識。