文章來源:Vue-cli 命令列工具分析
Vue-cli 命令列工具分析
Vue.js 提供一個官方命令列工具,可用於快速搭建大型單頁應用。vue-webpack-boilerplate,官方定義為:
full-featured Webpack setup with hot-reload, lint-on-save, unit testing & css extraction.
目錄結構:
├── README.md
├── build
│ ├── build.js
│ ├── utils.js
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ └── webpack.prod.conf.js
├── config
│ ├── dev.env.js
│ ├── index.js
│ └── prod.env.js
├── index.html
├── package.json
├── src
│ ├── App.vue
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── Hello.vue
│ └── main.js
└── static
config 環境配置
config 配置檔案用來配置 devServer 的相關設定,通過配置 NODE_ENV 來確定使用何種模式(開發、生產、測試或其他)
config
|- index.js #配置檔案
|- dev.env.js #開發模式
|- prod.env.js #生產模式
index.js
`use strict`
const path = require(`path`);
module.exports = {
dev: {
// 路徑
assetsSubDirectory: `static`, // path:用來存放打包後檔案的輸出目錄
assetsPublicPath: `/`, // publicPath:指定資原始檔引用的目錄
proxyTable: {}, // 代理示例: proxy: [{context: ["/auth", "/api"],target: "http://localhost:3000",}]
// 開發伺服器變數設定
host: `localhost`,
port: 8080,
autoOpenBrowser: true, // 自動開啟瀏覽器devServer.open
errorOverlay: true, // 瀏覽器錯誤提示 devServer.overlay
notifyOnErrors: true, // 配合 friendly-errors-webpack-plugin
poll: true, // 使用檔案系統(file system)獲取檔案改動的通知devServer.watchOptions
// source map
cssSourceMap: false, // develop 下不生成 sourceMap
devtool: `eval-source-map` // 增強除錯 可能的推薦值:eval, eval-source-map(推薦), cheap-eval-source-map, cheap-module-eval-source-map 詳細:https://doc.webpack-china.org/configuration/devtool
},
build: {
// index模板檔案
index: path.resolve(__dirname, `../dist/index.html`),
// 路徑
assetsRoot: path.resolve(__dirname, `../dist`),
assetsSubDirectory: `static`,
assetsPublicPath: `/`,
// bundleAnalyzerReport
bundleAnalyzerReport: process.env.npm_config_report,
// Gzip
productionGzip: false, // 預設 false
productionGzipExtensions: [`js`, `css`],
// source map
productionSourceMap: true, // production 下是生成 sourceMap
devtool: `#source-map` // devtool: `source-map` ?
}
}
dev.env.js
`use strict`
const merge = require(`webpack-merge`);
const prodEnv = require(`./prod.env`);
module.exports = merge(prodEnv, {
NODE_ENV: `"development"`
});
prod.env.js
`use strict`
module.exports = {
NODE_ENV: `"production"`
};
build Webpack配置
build
|- utils.js #程式碼段
|- webpack.base.conf.js #基礎配置檔案
|- webpack.dev.conf.js #開發模式配置檔案
|- webpack.prod.conf.js #生產模式配置檔案
|- build.js #編譯入口
實用程式碼段 utils.js
const config = require(`../config`)
const path = require(`path`)
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === `production`
? config.build.assetsSubDirectory // `static`
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path) // posix方法修正路徑
}
exports.cssLoaders = function (options) { // 示例: ({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
options = options || {};
// cssLoader
const cssLoader = {
loader: `css-loader`,
options: { sourceMap: options.sourceMap }
}
// postcssLoader
var postcssLoader = {
loader: `postcss-loader`,
options: { sourceMap: options.sourceMap }
}
// 生成 loader
function generateLoaders (loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] // 設定預設loader
if (loader) {
loaders.push({
loader: loader + `-loader`,
options: Object.assign({}, loaderOptions, { // 生成 options 物件
sourceMap: options.sourceMap
})
})
}
// 生產模式中提取css
if (options.extract) { // 如果 options 中的 extract 為 true 配合生產模式
return ExtractTextPlugin.extract({
use: loaders,
fallback: `vue-style-loader` // 預設使用 vue-style-loader
})
} else {
return [`vue-style-loader`].concat(loaders)
}
}
return { // 返回各種 loaders 物件
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders(`less`),
// 示例:[
// { loader: `css-loader`, options: { sourceMap: true/false } },
// { loader: `postcss-loader`, options: { sourceMap: true/false } },
// { loader: `less-loader`, options: { sourceMap: true/false } },
// ]
sass: generateLoaders(`sass`, { indentedSyntax: true }),
scss: generateLoaders(`sass`),
stylus: generateLoaders(`stylus`),
styl: generateLoaders(`stylus`)
}
}
exports.styleLoaders = function (options) {
const output = [];
const loaders = exports.cssLoaders(options);
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp(`\.` + extension + `$`),
use: loader
})
// 示例:
// {
// test: new RegExp(\.less$),
// use: {
// loader: `less-loader`, options: { sourceMap: true/false }
// }
// }
}
return output
}
exports.createNotifierCallback = function () { // 配合 friendly-errors-webpack-plugin
// 基本用法:notifier.notify(`message`);
const notifier = require(`node-notifier`); // 傳送跨平臺通知系統
return (severity, errors) => {
// 當前設定是隻有出現 error 錯誤時觸發 notifier 傳送通知
if (severity !== `error`) { return } // 嚴重程度可以是 `error` 或 `warning`
const error = errors[0]
const filename = error.file && error.file.split(`!`).pop();
notifier.notify({
title: pkg.name,
message: severity + `: ` + error.name,
subtitle: filename || ``
// icon: path.join(__dirname, `logo.png`) // 通知圖示
})
}
}
基礎配置檔案 webpack.base.conf.js
基礎的 webpack 配置檔案主要根據模式定義了入口出口,以及處理 vue, babel 等的各種模組,是最為基礎的部分。其他模式的配置檔案以此為基礎通過 webpack-merge 合併。
`use strict`
const path = require(`path`);
const utils = require(`./utils`);
const config = require(`../config`);
function resolve(dir) {
return path.join(__dirname, `..`, dir);
}
module.exports = {
context: path.resolve(__dirname, `../`), // 基礎目錄
entry: {
app: `./src/main.js`
},
output: {
path: config.build.assetsRoot, // 預設`../dist`
filename: `[name].js`,
publicPath: process.env.NODE_ENV === `production`
? config.build.assetsPublicPath // 生產模式publicpath
: config.dev.assetsPublicPath // 開發模式publicpath
},
resolve: { // 解析確定的擴充名,方便模組匯入
extensions: [`.js`, `.vue`, `.json`],
alias: { // 建立別名
`vue$`: `vue/dist/vue.esm.js`,
`@`: resolve(`src`) // 如 `@/components/HelloWorld`
}
},
module: {
rules: [{
test: /.vue$/, // vue 要在babel之前
loader: `vue-loader`,
options: vueLoaderConfig //可選項: vue-loader 選項配置
},{
test: /.js$/, // babel
loader: `babel-loader`,
include: [resolve(`src`)]
},{ // url-loader 檔案大小低於指定的限制時,可返回 DataURL,即base64
test: /.(png|jpe?g|gif|svg)(?.*)?$/, // url-loader 圖片
loader: `url-loader`,
options: { // 相容性問題需要將query換成options
limit: 10000, // 預設無限制
name: utils.assetsPath(`img/[name].[hash:7].[ext]`) // hash:7 代表 7 位數的 hash
}
},{
test: /.(mp4|webm|ogg|mp3|wav|flac|aac)(?.*)?$/, // url-loader 音視訊
loader: `url-loader`,
options: {
limit: 10000,
name: utils.assetsPath(`media/[name].[hash:7].[ext]`)
}
},{
test: /.(woff2?|eot|ttf|otf)(?.*)?$/, // url-loader 字型
loader: `url-loader`,
options: {
limit: 10000,
name: utils.assetsPath(`fonts/[name].[hash:7].[ext]`)
}
}
]
},
node: { // 是否 polyfill 或 mock
setImmediate: false,
dgram: `empty`,
fs: `empty`,
net: `empty`,
tls: `empty`,
child_process: `empty`
}
}
開發模式配置檔案 webpack.dev.conf.js
開發模式的配置檔案主要引用了 config 對於 devServer 的設定,對 css 檔案的處理,使用 DefinePlugin 判斷是否生產環境,以及其他一些外掛。
`use strict`
const webpack = require(`webpack`);
const config = require(`../config`);
const merge = require(`webpack-merge`);
const baseWebpackConfig = require(`./webpack.base.conf`);
const HtmlWebpackPlugin = require(`html-webpack-plugin`);
const portfinder = require(`portfinder`); // 自動檢索下一個可用埠
const FriendlyErrorsPlugin = require(`friendly-errors-webpack-plugin`); // 友好提示錯誤資訊
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
// 自動生成了 css, postcss, less 等規則,與自己一個個手寫一樣,預設包括了 css 和 postcss 規則
},
devtool: config.dev.devtool,// 新增元資訊(meta info)增強除錯
// devServer 在 /config/index.js 處修改
devServer: {
clientLogLevel: `warning`, // console 控制檯顯示的訊息,可能的值有 none, error, warning 或者 info
historyApiFallback: true, // History API 當遇到 404 響應時會被替代為 index.html
hot: true, // 模組熱替換
compress: true, // gzip
host: process.env.HOST || config.dev.host, // process.env 優先
port: process.env.PORT || config.dev.port, // process.env 優先
open: config.dev.autoOpenBrowser, // 是否自動開啟瀏覽器
overlay: config.dev.errorOverlay ? { // warning 和 error 都要顯示
warnings: true,
errors: true,
} : false,
publicPath: config.dev.assetsPublicPath, // 配置publicPath
proxy: config.dev.proxyTable, // 代理
quiet: true, // 控制檯是否禁止列印警告和錯誤 若使用 FriendlyErrorsPlugin 此處為 true
watchOptions: {
poll: config.dev.poll, // 檔案系統檢測改動
}
},
plugins: [
new webpack.DefinePlugin({
`process.env`: require(`../config/dev.env`) // 判斷生產環境或開發環境
}),
new webpack.HotModuleReplacementPlugin(), // 熱載入
new webpack.NamedModulesPlugin(), // 熱載入時直接返回更新的檔名,而不是id
new webpack.NoEmitOnErrorsPlugin(), // 跳過編譯時出錯的程式碼並記錄下來,主要作用是使編譯後執行時的包不出錯
new HtmlWebpackPlugin({ // 該外掛可自動生成一個 html5 檔案或使用模板檔案將編譯好的程式碼注入進去
filename: `index.html`,
template: `index.html`,
inject: true // 可能的選項有 true, `head`, `body`, false
}),
]
})
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port; // 獲取當前設定的埠
portfinder.getPort((err, port) => {
if (err) { reject(err) } else {
process.env.PORT = port; // process 公佈埠
devWebpackConfig.devServer.port = port; // 設定 devServer 埠
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ // 錯誤提示外掛
compilationSuccessInfo: {
messages: [`Your application is running here: http://${config.dev.host}:${port}`],
},
onErrors: config.dev.notifyOnErrors ? utils.createNotifierCallback() : undefined
}))
resolve(devWebpackConfig);
}
})
})
生產模式配置檔案 webpack.prod.conf.js
`use strict`
const path = require(`path`);
const utils = require(`./utils`);
const webpack = require(`webpack`);
const config = require(`../config`);
const merge = require(`webpack-merge`);
const baseWebpackConfig = require(`./webpack.base.conf`);
const CopyWebpackPlugin = require(`copy-webpack-plugin`);
const HtmlWebpackPlugin = require(`html-webpack-plugin`);
const ExtractTextPlugin = require(`extract-text-webpack-plugin`);
const OptimizeCSSPlugin = require(`optimize-css-assets-webpack-plugin`);
const env = process.env.NODE_ENV === `production`
? require(`../config/prod.env`)
: require(`../config/dev.env`)
const webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap, // production 下生成 sourceMap
extract: true, // util 中 styleLoaders 方法內的 generateLoaders 函式
usePostCSS: true
})
},
devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath(`js/[name].[chunkhash].js`),
chunkFilename: utils.assetsPath(`js/[id].[chunkhash].js`)
},
plugins: [
new webpack.DefinePlugin({ `process.env`: env }),
new webpack.optimize.UglifyJsPlugin({ // js 程式碼壓縮還可配置 include, cache 等,也可用 babel-minify
compress: { warnings: false },
sourceMap: config.build.productionSourceMap,
parallel: true // 充分利用多核cpu
}),
// 提取 js 檔案中的 css
new ExtractTextPlugin({
filename: utils.assetsPath(`css/[name].[contenthash].css`),
allChunks: false,
}),
// 壓縮提取出的css
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
// 生成 html
new HtmlWebpackPlugin({
filename: process.env.NODE_ENV === `production`
? config.build.index
: `index.html`,
template: `index.html`,
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
},
chunksSortMode: `dependency` // 按 dependency 的順序引入
}),
new webpack.HashedModuleIdsPlugin(), // 根據模組的相對路徑生成一個四位數的 hash 作為模組 id
new webpack.optimize.ModuleConcatenationPlugin(), // 預編譯所有模組到一個閉包中
// 拆分公共模組
new webpack.optimize.CommonsChunkPlugin({
name: `vendor`,
minChunks: function (module) {
return (
module.resource &&
/.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, `../node_modules`)
) === 0
)
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: `manifest`,
minChunks: Infinity
}),
new webpack.optimize.CommonsChunkPlugin({
name: `app`,
async: `vendor-async`,
children: true,
minChunks: 3
}),
// 拷貝靜態文件
new CopyWebpackPlugin([{
from: path.resolve(__dirname, `../static`),
to: config.build.assetsSubDirectory,
ignore: [`.*`]
}])]
})
if (config.build.productionGzip) { // gzip 壓縮
const CompressionWebpackPlugin = require(`compression-webpack-plugin`);
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: `[path].gz[query]`,
algorithm: `gzip`,
test: new RegExp(`\.(` + config.build.productionGzipExtensions.join(`|`) + `)$`),
threshold: 10240, // 10kb 以上大小的檔案才壓縮
minRatio: 0.8 // 最小比例達到 .8 時才壓縮
})
)
}
if (config.build.bundleAnalyzerReport) { // 視覺化分析包的尺寸
const BundleAnalyzerPlugin = require(`webpack-bundle-analyzer`).BundleAnalyzerPlugin;
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
}
module.exports = webpackConfig;
build.js 編譯入口
`use strict`
process.env.NODE_ENV = `production`; // 設定當前環境為生產環境
const ora = require(`ora`); //loading...進度條
const rm = require(`rimraf`); //刪除檔案 `rm -rf`
const chalk = require(`chalk`); //stdout顏色設定
const webpack = require(`webpack`);
const path = require(`path`);
const config = require(`../config`);
const webpackConfig = require(`./webpack.prod.conf`);
const spinner = ora(`正在編譯...`);
spinner.start();
// 清空資料夾
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err;
// 刪除完成回撥函式內執行編譯
webpack(webpackConfig, function (err, stats) {
spinner.stop();
if (err) throw err;
// 編譯完成,輸出編譯檔案
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + `
`);
//error
if (stats.hasErrors()) {
console.log(chalk.red(` 編譯失敗出現錯誤.
`));
process.exit(1);
}
//完成
console.log(chalk.cyan(` 編譯成功.
`))
console.log(chalk.yellow(
` file:// 無用,需http(s)://.
`
))
})
})