基於Vue-cli的多目錄項目配置

直接用vue-cli創建的項目可以創建一個單頁面應用,開發環境和生產環境都是以一個單獨的項目爲目錄的。在寫一些有共性的模塊時需要將所有組件放在同一個大的框架下的同時又需要每個模塊可以進行單獨的啓動和打包。此時就需要進行個性化配置。

場景:需要src下不是一個單獨的模塊而是一組同一個大的項目的模塊組。將單獨的模塊組放在文件夾module下,而將一些通用的通用css、通用js配置或方法、通用圖片、以及vue組建等通用模塊放在common中。此時構建的目錄結構大致如下:
—build
—config
—node_modules
—src
——common
————js
————css
————pic
————components
——module
————head
—————index.html
—————src
———————assets
———————components
———————router
———————app.vue
———————main.js
————face
————hand
————body
————foot
——package
—package.json

需要對webpack打包文件進行配置,可以使用以下命令啓動和打包單獨的模塊:

npm run dev <module name>
npm run build <module name>

對單獨的每個模塊進行開發和打包。例如:運行npm run dev demo來對demo模塊進行開發模式的啓動。爲實現以上目的,首先要看一下vue-cli自帶的原配置。首先先查看原先的package.json來看看自帶的配置都做了寫什麼。package.json中關於script的配置如下:

"scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "build": "node build/build.js"
  }

關於package.json中的script配置:npm 允許在package.json文件裏面,使用scripts字段定義腳本命令
也就是說相當於運行npm run命令時會按照package.json中的命令來進行腳本運行而無需自己在命令行中輸入那麼長的腳本命令。關於npm script想了解更多的話可以看看阮一峯老師的npm scripts 使用指南

首先對開發環境的dev命令運行進行配置更改,原配置中執行dev實際上運行的是
webpack-dev-server --inline --progress --config build/webpack.dev.conf.js
這條命令啓動webpack-dev-server模塊並將配置參數傳入,關於配置參數--是webpack聲明參數的方式,在我們運行dev時傳入的參數包括:

  • inline: 布爾值,用於在 dev-server 的兩種不同模式之間切換。默認情況下,應用程序啓用內聯模式(inline mode)。這意味着一段處理實時重載的腳本被插入到你的包(bundle)中,並且構建消息將會出現在瀏覽器控制檯。也可以使用 iframe 模式,它在通知欄下面使用 <iframe> 標籤,包含了關於構建的消息。當inline的值爲false時啓用iframe模式。
  • progess:該配置只用於命令行工具,用於將運行進度輸出到控制檯。
  • config:配置信息,在此處傳入build/webpack.dev.conf.js。運行由配置文件導出的函數,並且等待 Promise 返回。便於需要異步地加載所需的配置變量。

那麼對於開發模式的修改就應當時從配置文件webpack.dev.conf.js入手了。先來看原有配置

const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')

const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)

// 與webpack.base.conf進行合併
const devWebpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
  },
  // cheap-module-eval-source-map is faster for development
  devtool: config.dev.devtool,

  // these devServer options should be customized in /config/index.js
  devServer: {
    clientLogLevel: 'warning',
    historyApiFallback: {
      rewrites: [
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
      ],
    },
    hot: true,
    contentBase: false, // since we use CopyWebpackPlugin.
    compress: true,
    host: HOST || config.dev.host,
    port: PORT || config.dev.port,
    open: config.dev.autoOpenBrowser,
    overlay: config.dev.errorOverlay
      ? { warnings: false, errors: true }
      : false,
    publicPath: config.dev.assetsPublicPath,
    proxy: config.dev.proxyTable,
    quiet: true, // necessary for FriendlyErrorsPlugin
    watchOptions: {
      poll: config.dev.poll,
    }
  },
  plugins: [
    // 允許在編譯時(compile time)配置的全局常量
    new webpack.DefinePlugin({
      'process.env': require('../config/dev.env')
    }),
    // 啓用模塊熱替換
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
    // 在輸出階段時,遇到編譯錯誤跳過
    new webpack.NoEmitOnErrorsPlugin(),
    // https://github.com/ampedandwired/html-webpack-plugin
    // 簡單創建 HTML 文件,用於服務器訪問
    new HtmlWebpackPlugin({
      // 要將HTML寫入的文件
      filename: 'index.html',
      // 本地模板文件的位置,支持加載器(如handlebars、ejs、undersore、html等)
      template: 'index.html',
      // 傳遞true或'body'所有javascript資源將被放置在body元素的底部。'head'將腳本放在head元素中
      inject: true
    }),
    // copy custom static assets
    // 將單個文件或整個目錄複製到構建目錄
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.dev.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
  ]
})

module.exports = new Promise((resolve, reject) => {
  portfinder.basePort = process.env.PORT || config.dev.port
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
      // publish the new Port, necessary for e2e tests
      process.env.PORT = port
      // add port to devServer config
      devWebpackConfig.devServer.port = port

      // Add FriendlyErrorsPlugin
      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
        compilationSuccessInfo: {
          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
        },
        onErrors: config.dev.notifyOnErrors
        ? utils.createNotifierCallback()
        : undefined
      }))

      resolve(devWebpackConfig)
    }
  })
})
]
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')

const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)

// 與webpack.base.conf進行合併
const devWebpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
  },
  // cheap-module-eval-source-map is faster for development
  devtool: config.dev.devtool,

  // these devServer options should be customized in /config/index.js
  devServer: {
    clientLogLevel: 'warning',
    historyApiFallback: {
      rewrites: [
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
      ],
    },
    hot: true,
    contentBase: false, // since we use CopyWebpackPlugin.
    compress: true,
    host: HOST || config.dev.host,
    port: PORT || config.dev.port,
    open: config.dev.autoOpenBrowser,
    overlay: config.dev.errorOverlay
      ? { warnings: false, errors: true }
      : false,
    publicPath: config.dev.assetsPublicPath,
    proxy: config.dev.proxyTable,
    quiet: true, // necessary for FriendlyErrorsPlugin
    watchOptions: {
      poll: config.dev.poll,
    }
  },
  plugins: [
    // 允許在編譯時(compile time)配置的全局常量
    new webpack.DefinePlugin({
      'process.env': require('../config/dev.env')
    }),
    // 啓用模塊熱替換
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
    // 在輸出階段時,遇到編譯錯誤跳過
    new webpack.NoEmitOnErrorsPlugin(),
    // https://github.com/ampedandwired/html-webpack-plugin
    // 簡單創建 HTML 文件,用於服務器訪問
    new HtmlWebpackPlugin({
      // 要將HTML寫入的文件
      filename: 'index.html',
      // 本地模板文件的位置,支持加載器(如handlebars、ejs、undersore、html等)
      template: 'index.html',
      // 傳遞true或'body'所有javascript資源將被放置在body元素的底部。'head'將腳本放在head元素中
      inject: true
    }),
    // copy custom static assets
    // 將單個文件或整個目錄複製到構建目錄
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.dev.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
  ]
})

module.exports = new Promise((resolve, reject) => {
  portfinder.basePort = process.env.PORT || config.dev.port
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
      // publish the new Port, necessary for e2e tests
      process.env.PORT = port
      // add port to devServer config
      devWebpackConfig.devServer.port = port

      // Add FriendlyErrorsPlugin
      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
        compilationSuccessInfo: {
          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
        },
        onErrors: config.dev.notifyOnErrors
        ? utils.createNotifierCallback()
        : undefined
      }))

      resolve(devWebpackConfig)
    }
  })
})

既然要在開發時每個模塊要進行單獨的開發那模塊啓動時應該根據傳入的模塊名進行啓動,而不是固定的main.js.因此需要對入口文件進行改動。入口文件的配置在webpack.base.conf中,配置如下:

entry: {
    app: './src/main.js'
}

應該改爲:

entry: {
    app: './src/module/' + entrydir + '/src/main.js'
}

注意webpack.dev.conf.js中是沒有entry配置的,這個文件直接調用了webpack.base.conf的配置,因此可以直接添加進配置對象中。其中entryname是需要啓動的模塊項目名稱。後續會進行詳細說明。
在這裏插入圖片描述

當然了也可以直接改webpack.base.conf的配置。但是從圖中的merge函數其實是可以看出來更改dev配置會覆蓋並添加上webpack.base.conf的配置的

除了需要修改服務啓動的入口文件目錄以外,還有一個需要注意的地方,webpack.dev.conf.js中的生成html的插件HtmlWebpackPlugin中模版屬性需要替換爲模塊內相應的index.html而不是根目錄下的index.html
在這裏插入圖片描述
如果你的index.html沒有進行更改那可能用哪個index.html文件作爲模版都是一樣的,但是如果改動了則一定要用當前項目的index.html作爲生成html的模版。

其中entrydir爲項目名,現在的問題是把項目名通過命令行傳入配置文件中。接下來研究如何通過命令行獲取目錄名的問題。
關於命令行傳入參數的方式,經過了許多嘗試。

  • 最開始使用的方式是--key value在命令行後面加上--name demo結果會被提示demo這個模塊沒有安裝。根據報錯提示可以看出webpack並未將demo看成是name的值而是識別爲一個單獨的模塊。在看了webpack-dev-server.js時發現該模塊使用命令行工具yargs模塊進行參數讀取而我們自定義的參數name是不會進行識別讀取的。會有一下報錯:
    在這裏插入圖片描述
  • 第二個使用的方式是使用DefinePlugin插件自定義數據然後在命令行中進行參數定義。
  new webpack.DefinePlugin({
    'process.env': require('../config/dev.env')
  })

可以看到自定義參數是託管在process.env中。此處自定義的參數配置定義在config/dev.env中。打開該文件發現參數如下:

'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
  NODE_ENV: '"development"',
  name: ''
})

在參數NODE_ENV下加上我們自定義的參數name, 然後在命令行傳入name,更改package.json如下:

    "dev": "entryname=demo webpack-dev-server --inline --progress --config build/webpack.dev.conf.js"    

使用後果然可以成功啓動demo模塊
在這裏插入圖片描述

然而使用這個方式有一個比較大的bug就是隻能用

"dev": "entryname=demo webpack-dev-server --inline --progress --config build/webpack.dev.conf.js"    

而不能使用

"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js entryname=demo"   

這就意味着不能使用

npm run dev entryname=demo

來啓動,而如果試圖將entryname=demo加在dev之前,則會
在這裏插入圖片描述

所以不符合我最初的npm run dev <some code>的啓動方式,pass掉

  • 使用webpack自帶的環境選項,當 webpack 配置對象導出爲一個函數時,可以向起傳入一個"環境對象"。例如:
webpack --env.production    # 設置 env.production == true
webpack --env.platform=web  # 設置 env.platform == "web"

關於–env的參數用法官方給出了以下示例:
在這裏插入圖片描述

關於webpack的命令行接口參數可以點擊這裏查看官方文檔

按照官方文檔對package.json進行更改

  "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js -- --env.name=demo"   

因爲傳參數是在結尾傳遞的可以通過命令行傳遞

npm run dev -- --env.name=demo

不需要對dev進行更改也可以運行

再將 webpack.dev.conf.js代碼進行更改,需要更改的部分是之前提到的entry和HtmlWebpackPlugin插件的template屬性,命令行參數可以用argv進行參數接接收。關於process.argv官方描述如下:

process.argv 屬性返回一個數組,這個數組包含了啓動Node.js進程時的命令行參數。第一個元素爲process.execPath。如果需要獲取argv[0]的值請參見 process.argv0。第二個元素爲當前執行的JavaScript文件路徑。剩餘的元素爲其他命令行參數。

關於官方process.argv 的詳細解釋請點擊這裏

用如下語句獲取模塊名作爲路徑傳給入口文件配置和index文件模版配置。更改方式上文已經提到了,這裏不再贅述。

const entrydir = process.argv[process.argv.length - 1].replace(/^(\S)*=/, '')

運行結果如下:
在這裏插入圖片描述

效果不錯是最初想要的npm run dev <some code>啓動開發模式的方式。但是要輸入npm run dev -- --env.name=demo這麼長一串好像不太合理。最好是輸入npm run demo就可以啓動。
那能不能配置

    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js -- --env.name=",    

然後運行npm run demo,雖然看起來很美好但顯然不能,因爲有空格就會識別爲兩個部分,所以又想只輸入模塊名又要把模塊名作爲參數錄入給命令行就只能通過另一個js文件來起到“中間人”的作用。具體方式是:

  • 更改package.json
    在這裏插入圖片描述
  • 構建用來將參數傳遞給配置文件devServer,首先在build目錄下創建一個js文件取一個你認爲合適的名字用dev配置爲啓動的命令簡寫。devServer.js內容如下。
const cprocess = require('child_process')
let entryDir = process.argv[process.argv.length-1]
let cmd = 'npm run startdev -- --env.name=' + entryDir 
let dev = cprocess.exec(cmd, {detached: true} ,function(error, stdout, stderr) {
        if(error) console.log(error)   
})
dev.stdout.pipe(process.stdout)
dev.stderr.pipe(process.stderr)

這裏寫的比較簡單也可以用命令行工具作出更多樣化的功能。這裏實現的功能是接收參數開啓子進程將參數拼接在子進程中運行,並將子進程的標準輸出和標準錯誤與父進程的標準輸出和標準錯誤通過管道進行鏈接,這樣就可以及時的獲取子進程的輸出及報錯信息了。關於node的子進程使用方式想了解更多的話可以點擊這裏
到這裏開發模式的相關個性化配置就告一段落。下面來看看生產模式下的個性化配置, 同上先從package.json入手看看我們npm run build之後到底發生了什麼,scripts配置如下:

    "build": "node build/build.js"

可以看到實際上運行build命令後會啓動build下的文件build.js,那就來看看build.js

'use strict'
require('./check-versions')()

process.env.NODE_ENV = 'production'

const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')

// 出現加載標示
const spinner = ora('building for production...')
spinner.start()

// rimraf 包的作用:以包的形式包裝rm -rf命令,用來刪除文件和文件夾的,不管文件夾是否爲空,都可刪除
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
  webpack(webpackConfig, (err, stats) => {
    // 加載標識結束
    spinner.stop()
    if (err) throw err
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
      chunks: false,
      chunkModules: false
    }) + '\n\n')

    if (stats.hasErrors()) {
      console.log(chalk.red('  Build failed with errors.\n'))
      process.exit(1)
    }

    console.log(chalk.cyan('  Build complete.\n'))
    console.log(chalk.yellow(
      '  Tip: built files are meant to be served over an HTTP server.\n' +
      '  Opening index.html over file:// won\'t work.\n'
    ))
  })
})

總的來說build.js幹了這麼幾件事

  • 進行版本檢測不合適就報錯
  • 使用ora進行加載圖標的渲染,打包完成就顯示打包結果
  • 把webpack.prod.conf配置傳給webpack進行打包,並把相關結果傳給標準輸出打印在屏幕上。有錯誤報錯誤沒錯誤,沒錯誤報信息。

所以如果要進行打包修改重點是第三個更改配置文件。但是懷着求知的心態,也來看看前兩點webpack是怎麼做的,如果並不想了解可以往下瀏覽直接看webpack.prod.conf的配置即可:

  • 版本檢測的文件check-versions
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')
// 定義子進程exec並將返回進程的輸出流
// execSync返回: <Buffer> | <string> 該命令的 stdout
function exec (cmd) {
  return require('child_process').execSync(cmd).toString().trim()
}
const versionRequirements = [
  {
    name: 'node',
    // 返回一個標準的版本號,且去掉兩邊的空格
    currentVersion: semver.clean(process.version),
    // 引入package.json中的engines.node 制定node版本的範圍
    versionRequirement: packageConfig.engines.node
  }
]

// 檢測是否存在npm的路徑
// 如果有則在versionRequirements中添加npm相關信息對象
if (shell.which('npm')) {
  versionRequirements.push({
    name: 'npm',
    currentVersion: exec('npm --version'),
    versionRequirement: packageConfig.engines.npm
  })
}

module.exports = function () {
  const warnings = []

  for (let i = 0; i < versionRequirements.length; i++) {
    const mod = versionRequirements[i]
    // satisfies(version, range):如果版本滿足範圍,則返回true
    // 判定當前版本是否滿足版本範圍, 不滿足則將報錯信息存儲在warnings中
    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
      warnings.push(mod.name + ': ' +
        chalk.red(mod.currentVersion) + ' should be ' +
        chalk.green(mod.versionRequirement)
      )
    }
  }

  // 如果存在錯誤則進行報錯
  if (warnings.length) {
    console.log('')
    console.log(chalk.yellow('To use this template, you must update following to modules:'))
    console.log()

    for (let i = 0; i < warnings.length; i++) {
      const warning = warnings[i]
      console.log('  ' + warning)
    }

    console.log()
    process.exit(1)
  }
}

  • 關於org加載圖標的問題主要用來實現一個命令行下的加載效果。大致用法如下:
const ora = require('ora');

const spinner = ora('Loading unicorns').start();

setTimeout(() => {
	spinner.color = 'yellow';
	spinner.text = 'Loading rainbows';
}, 1000);

以上命令1s後加載圖標的顏色和內容會改變。但是關於ora更多的用法還是應該去看看官方的git,git傳送門在此

  • 當然最重要的部分還是得看看webpack.prod.conf配置,因爲配置其實大體和webpack.dev.conf差不多所以沒有再重新註釋一遍的必要了,直接來說如何修改。

先來明確目的是爲了將參數從命令行傳入文件中,等等這個需求好像上文見過?沒錯這個和之前開發模式的需求相同所以需要更改的位置也一樣分別是入口文件entry和HtmlWebpackPlugin的template。但是這裏就不要第三個文件介入因爲直接傳參數默認就會傳給你啓動的這個文件。

 "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",  
 "build": "node build/build.js",

第一種情況需要把參數傳給webpack.dev.js而不是啓動的webpack-dev-server 因此需要加標示來聲明這一點參數是傳給配置文件的而不是啓動的文件。第二個生產命令則不需要 因爲命令本來就是傳給build.js和該環境下啓動的webpack及其配置文件的。只需要做好接收就好了。接收參數方式和之前一樣。不再贅述。這裏放運行結果圖:
在這裏插入圖片描述

至此,配置已經基本完成了。後續可以引入命令行達到更好的體驗。

別名配置:修改webpack.base.conf文件可以使得文件引用更加方便,而且@如果不對@進行修改則其目錄指向爲'@': resolve('src'),指向的時項目根目錄的src文件而不是單模塊目錄下的src,就會造成引用錯誤。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章