Vue-cli 命令列工具分析

Oliveryoung發表於2019-02-16

文章來源:Vue-cli 命令列工具分析


Vue-cli 命令列工具分析

Vue.js 提供一個官方命令列工具,可用於快速搭建大型單頁應用。vue-webpack-boilerplate,官方定義為:

  1. 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)://.
`
    ))
  })

})

相關文章