快速入門vue-cli配置

陳杰夫發表於2019-03-04
作為一名使用了一段時間Vue.js的新手,相信和不少初入Vue的朋友一樣,都對Vue-cli的配置一知半解。後來通過對webpack的學習,也算是對腳手架的配置有了一定的瞭解,所以也想把這段時間自己的成果分享給大家,希望能和大家一起進步。

有兩點要說明的:

  1. 閱讀本文需要了解一點點webpack的知識,至少要entry,output,module,plugins都是做什麼,以及一些常用的loader和plugins;
  2. 本文使用的是最新版的vue,配置可能會和大家的有所不同,不過差距不會太大,不影響閱讀;

一.起步

先放一張自己整理的簡易腦圖:

快速入門vue-cli配置

Vue-cli有兩個檔案——buildconfig:build檔案包含了腳手架在開發環境和生產環境下webpack該如何配置。config檔案則包含了build檔案下webpack具體配置的值。換句話說,build下的webpack配置的值要引入config後才能獲取到。

config資料夾下一共有三個檔案

  • dev.env.js: 匯出開發環境名稱;
  • prod.env.js: 匯出生產環境名稱;
  • index.js: 匯出不同環境的具體配置;


build資料夾下一共有七個檔案

  • build.js: 編譯時的入口檔案,當執行npm run build時其實就是執行node build/build.js(在package.json中);
  • check-versions.js: 編譯程式碼時執行的確認node和npm版本的檔案,如果版本不符,則停止編譯;
  • utils.js:這個檔案有兩個作用,一是作為vue-loader的配置來使用;另一個是用來給開發環境和生產環境配置loader;
  • vue-loader.conf.js:vue-loader的配置,用在webpack.base.conf.js中;
  • webpack.base.conf.js:vue-cli腳手架的基礎webpack配置,通過與webpack.dev.conf.js和webpack.prod.conf.js兩個配置檔案的合併(合併方式我會在下一章來講)來實現“不重複原則(Don't repeat yourself - DRY),不會在不同的環境中配置相同的程式碼”
  • webpack.dev.conf.js:開發環境下的webpack的配置;
  • webpack.prod.conf.js:生產環境下的webpack的配置;


二.config檔案

1.prod.env.js:

//匯出一個物件,物件有一個當前node環境的屬性,值為“production”(生產環境)
module.exports = {  NODE_ENV: '"production"'}複製程式碼

2.dev.env.js:

//匯出另一個物件,屬性為當前的node環境,值為“development”(開發環境)
const merge = require('webpack-merge')const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {  NODE_ENV: '"development"'})
複製程式碼

  • 這裡要著重說一下webpack-merge這個包,這個包的作用是來合併兩個配置檔案物件並生成一個新的配置檔案,有點兒類似於es6的Object.assign()方法。如果合併的過程中遇到衝突的屬性,第二個引數的屬性值會覆蓋第一個引數的屬性值。
  • 前面寫到webpack.base.conf.js與webpack.dev.conf.js和webpack.prod.conf.js的合併也用到了webpack-merge。Vue-cli將一些通用的配置抽出來放在一個檔案內(webpack.base.conf.js),在對不同的環境配置不同的程式碼,最後使用webpack-merge來進行合併,減少重複程式碼。

關於更多webpack-merge請點選www.npmjs.com/package/web…

3.index.js:

index.js作為具體的配置值,我覺得沒必要把程式碼貼出來了,大家可以拿上面的的腦圖或者自己專案裡的檔案來結合我後面要說的程式碼來看。

三.build檔案

1.check.versions.js:

//chalk 是一個用來在命令列輸出不同顏色文字的包,可以使用chalk.yellow("想新增顏色的文字....")
//來實現改變文字顏色的;
const chalk = require('chalk')

//semver 的是一個語義化版本檔案的npm包,其實它就是用來控制版本的;
const semver = require('semver')const packageConfig = require('../package.json')

//一個用來執行unix命令的包
const shell = require('shelljs')

//child_process 是Node.js提供了衍生子程式功能的模組,execSync()方法同步執行一個cmd命令,
//將返回值的呼叫toString和trim方法
function exec (cmd) {  
    return require('child_process').execSync(cmd).toString().trim()
}

const versionRequirements = [
  {    name: 'node',

    //semver.clean()方法返回一個標準的版本號,切去掉兩邊的空格,比如semver.clean(" =v1.2.3 ")
    //返回"1.2.3",此外semver還有vaild,satisfies,gt,lt等方法,
    //這裡檢視https://npm.taobao.org/package/semver可以看到更多關於semver方法的內容  
    currentVersion: semver.clean(process.version),    
    versionRequirement: packageConfig.engines.node  
  }
]
//shell.which方法是去環境變數搜尋有沒有引數這個命令
if (shell.which('npm')) {  
    versionRequirements.push({    
        name: 'npm',
        //執行"npm --version"命令    
        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]
    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)  }}複製程式碼

2.utils.js:

const path = require('path')const config = require('../config')

//這個plugin的作用是將打包後生成的css檔案通過link的方式引入到html中,如果不適用這個外掛css程式碼會
//放到head標籤的style中
const ExtractTextPlugin = require('extract-text-webpack-plugin')

const packageConfig = require('../package.json')

//process.env.NODE_ENV是一個環境變數,它是由webpack.dev/prod.conf.js這兩個檔案宣告的;
//這裡的意思是判斷當前是否是開發環境,如果是就把config下index.js檔案中build.assetsSubDirectory或
//dev.assetsSubDirectory的值賦給assetsSubDirectory 
exports.assetsPath = function (_path) {  
    const assetsSubDirectory = process.env.NODE_ENV === 'production'    
    ? config.build.assetsSubDirectory    
    : config.dev.assetsSubDirectory  
  //path.posix.join是path.join的一種相容性寫法,它的作用是路徑的拼接,這裡返回的是"static/_path"
  return path.posix.join(assetsSubDirectory, _path
)}
//cssLoaders的作用是匯出一個供vue-loader的options使用的一個配置;
exports.cssLoaders = function (options) {  
    options = options || {}
    const cssLoader = {    
        loader: 'css-loader',    
        options: {      
            sourceMap: options.sourceMap    
        }  
    }
    const postcssLoader = {    
        loader: 'postcss-loader',    
        options: {      
            sourceMap: options.sourceMap    
        }  
    }
function generateLoaders (loader, loaderOptions) {    
    const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
    if (loader) {     
         loaders.push({        
            loader: loader + '-loader',       
            options: Object.assign({}, loaderOptions, {          
                sourceMap: options.sourceMap        
            })      
        })    
    }
    if (options.extract) {      
        return ExtractTextPlugin.extract({        
            use: loaders,        
            fallback: 'vue-style-loader'      
        })    
    } else {      
        return ['vue-style-loader'].concat(loaders)    
        }  
    }
    return {    
        css: generateLoaders(),    
        postcss: generateLoaders(),    
        less: generateLoaders('less'),    
        sass: generateLoaders('sass', { indentedSyntax: true }),    
        scss: generateLoaders('sass'),    
        stylus: generateLoaders('stylus'),    
        styl: generateLoaders('stylus')  
    }
}

// styleLoaders是用來給webpack提供所有和css相關的loader的配置,它也使用了cssLoaders()方法;
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    
        })  
    }
  return output
}

//'node-notifier'是一個跨平臺系統通知的頁面,當遇到錯誤時,它能用系統原生的推送方式給你推送資訊
exports.createNotifierCallback = () => {  
    const notifier = require('node-notifier')
    return (severity, errors) => {    
        if (severity !== 'error') return
        const error = errors[0]    
        const filename = error.file && error.file.split('!').pop()
        notifier.notify({      
            title: packageConfig.name,      
            message: severity + ': ' + error.name,      
            subtitle: filename || '',     
             icon: path.join(__dirname, 'logo.png')    
        })  
    }
}
複製程式碼

這裡可能有的朋友不瞭解cssLoaders()和styleLoaders()這兩個方法返回的是個什麼東西,我在這裡簡單的寫一下:

  • cssLoaders方法根據傳進來的引數(options)是否有extract屬性來返回不同的值,如果你看了後面的程式碼你就會知道在生產模式下extract屬性為true,開發模式下為false。也就是說,在生產模式下返回的是一個類似於這樣的陣列:

ExtractTextPlugin.extract({
    use: ["css-loader","less-loader","sass-loader"...],
    fallback: 'vue-style-loader'
})
複製程式碼

      這些css程式碼打包以link的方式放到HTML中。當然了,use的值確切的說應該是這樣:

[ { loader: 'css-loader', options: { sourceMap: true } }, { loader: 'less-loader', options: { sourceMap: true } } ]

       我為了方便看就簡寫了。

       而在開發模式下,cssLoaders返回的是:

["vue-style-loader","css-loader","less-loader","sass-loader"...] //我還是簡寫了

  • styleLoaders方法返回的值就簡單了,它返回的就是webpack中module裡常用的配置格式:

[
    {
        test: /\.css$/,
        use: [ 'style-loader', 'css-loader' ]
    },
    ...
]
複製程式碼

3.vue-loader.conf.js:

const utils = require('./utils')
const config = require('../config')

//不同環境為isProduction 賦值: 生產環境為true,開發環境為false
const isProduction = process.env.NODE_ENV === 'production'

//不同環境為sourceMapEnabled 賦值: 這裡都為true
const sourceMapEnabled = isProduction  
? config.build.productionSourceMap  
: config.dev.cssSourceMap
//匯出vue-loader的配置,這裡我們用了utils檔案中的cssLoaders();
module.exports = {  
    loaders: utils.cssLoaders({    
        sourceMap: sourceMapEnabled,    
        extract: isProduction  
    }),  
    cssSourceMap: sourceMapEnabled,
    cacheBusting: config.dev.cacheBusting,

    //transformToRequire的作用是在模板編譯的過程中,編譯器可以將某些屬性,如src轉換為require呼叫;  
    transformToRequire: {    
        video: ['src', 'poster'],    
        source: 'src',    
        img: 'src',    
        image: 'xlink:href'  
    }
}
複製程式碼

4.webpack.base.conf.js

const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')

//resolve這個函式返回的是當前目錄下"../dir"這個資料夾,__dirname指的是當前檔案所在路徑
function resolve (dir) {  return path.join(__dirname, '..', dir)}

module.exports = {
    //返回專案的根路徑  
    context: path.resolve(__dirname, '../'),
    //入口檔案  
    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: {
        //自動解析擴充套件,比如引入對應的檔案,js,vue,json的字尾名就可以省略了    
        extensions: ['.js', '.vue', '.json'],    
        alias: {
            //精準匹配,使用vue來替代vue/dist/vue.esm.js      
            'vue$': 'vue/dist/vue.esm.js',
            //使用@替代src路徑,當你引入src下的檔案是可以使用import XXfrom "@/xx"
            '@': resolve('src'),    
        }  
    },

    //一些loader配置,避免篇幅過長我省略一部分,大家可以看自己的檔案 
    module: {    
        rules: [     
            {        
                test: /\.vue$/,        
                loader: 'vue-loader',        
                options: vueLoaderConfig      
            },      
            {        
                test: /\.js$/,        
                loader: 'babel-loader',        
                include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]      
            },      
            ......    
        ]  
    },

    //node裡的這些選項是都是Node.js全域性變數和模組,這裡主要是防止webpack注入一些Node.js的東西到vue中  
    node: {    
        setImmediate: false,    
        dgram: 'empty',    
        fs: 'empty',    
        net: 'empty',    
        tls: 'empty',    
        child_process: 'empty'  
    }
}複製程式碼

5.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')

//一個更友好的展示webpack錯誤提示的外掛
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')

//一個自動檢索埠的包
const portfinder = require('portfinder')

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

const devWebpackConfig = merge(baseWebpackConfig, {  
    module: {    
        rules: utils.styleLoaders({ 
            sourceMap: config.dev.cssSourceMap,
            usePostCSS: true 
        })  
    },  
    
    devtool: config.dev.devtool,
    // devServer的配置大家看文件就好了 
    devServer: {    
        clientLogLevel: 'warning',    
        historyApiFallback: {      
            rewrites: [        
                { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },      
            ],    
        },    
        hot: true,    
        contentBase: false,   
        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,    
        watchOptions: {      
            poll: config.dev.poll,    
        }  
    },  
    plugins: [
    
        //還記得之前說的生產環境和開發環境的變數在哪兒定義的嗎?對,就是這裡    
        new webpack.DefinePlugin({      
            process.env: require('../config/dev.env')    
        }),

        //模組熱替換的外掛,修改模組不需要重新整理頁面    
        new webpack.HotModuleReplacementPlugin(),

        //當使用HotModuleReplacementPlugin時,這個外掛會顯示模組正確的相對路徑    
        new webpack.NamedModulesPlugin(),

        //在編譯出錯時,使用NoEmitOnErrorsPlugin來跳過輸出階段,這樣可以確保輸出資源不會包含錯誤
        new webpack.NoEmitOnErrorsPlugin(),   
        new HtmlWebpackPlugin({      
            filename: 'index.html',      
            template: 'index.html',      
            inject: true    
        }),    

        // 將static資料夾和裡面的內容拷貝到開發模式下的路徑,比如static下有個img資料夾,裡面有張圖片
        // 我們可以這樣訪問:localhost:8080/static/img/logo.png    
        new CopyWebpackPlugin([     
            {        
                from: path.resolve(__dirname, '../static'),        
                to: config.dev.assetsSubDirectory,        
                ignore: ['.*']      
            }    
        ])  
    ]
})

//這裡主要是做埠的檢索以及npm run dev後對錯誤的處理,我們可以看這裡使用了前面引入的
//'friendly-errors-webpack-plugin'外掛
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)
    }
  })
})複製程式碼

關於devServer有兩點要說明一下:

  • contentBase是來告訴伺服器在哪裡提供靜態的內容,這裡我們使用false的原因是使用了“copy-webpack-plugin”外掛,不需要使用contentBase了;
  • quiet開啟後(true),除了初始啟動資訊之外的任何內容都不會被列印到控制檯,即使是webpack 的錯誤或警告在控制檯也不可見。不過我們用了'friendly-errors-webpack-plugin'外掛,就可以設為true了。


6.webpack.prod.conf.js 

經過前面這麼多程式碼的分析,其實webpack.prod.conf.js的配置已經很簡單了,大致跟webpack.dev.conf.js的配置方式差不多,就是多了幾個plugins:

  • UglifyJsPlugin是用來壓縮JS程式碼
  • optimize-css-assets-webpack-plugin是用來壓縮css程式碼
  • HashedModuleIdsPlugin會根據模組的相對路徑生成一個四位數的hash作為模組id
  • ModuleConcatenationPlugin可以預編譯所有模組到一個包中,加快瀏覽器的執行速度
  • CommonsChunkPlugin拆分公共模組,vue裡拆分了vendor,manifest和app三個模組
  • compression-webpack-plugin gzip壓縮
  • webpack-bundle-analyzer可以檢視打包的具體情況,比如打了多少個包,每個包多大等

好了,plugins的介紹到此結束,接下來就是最後一個檔案,也是npm run build編譯時的入口檔案——build.js了。

同樣的,build.js檔案其實也沒什麼可說的了,無非就是執行webpack.prod.conf.js檔案,遇到錯誤時在命令列提示。需要注意的是,build.js裡引入了“rimraf”的包,它的作用是每次編譯時清空dist檔案,避免多次編譯時造成資料夾的重複和混亂。

四.結尾

到這裡其實關於Vue-cli配置的分析基本結束了,相信瞭解webpack的朋友看起來一定非常簡單,配置主要麻煩的地方在於低耦合導致經常需要來回翻檔案才能看懂配置,如果大家結合著文章開頭的腦圖看可能會相對容易些。

一個壞訊息是這個文章釋出的時候webpack4.0已經上線了,Vue-cli新版也進入了Beta測試階段,所以這篇文章大家看看就好,瞭解一下思路,馬上配置又會更新的......