從vue-cli中看node.js和webpack的運作

前言

vue-cli和webpack結合的腳手架挺好用的,但是初次使用對於其中的配置和npm包的引用總是會一臉懵逼,這篇文章是對其中一些相關模塊的簡單分析。

主要目的是加深我自己對webpack和node.js的認知。

正文

1.項目結構

vue-cli的配置文件主要在build和config文件夾中,其中config文件夾主要是放一些環境變量,webpack的路徑等等一些參數。

| -- build
    | -- build.js
    | -- check-version.js
    | -- dev-client.js
    | -- dev-server.js
    | -- utils.js
    | -- vue-loader.conf.js
    | -- webpack.base.conf.js
    | -- webpack.dev.conf.js
    | -- webpack.prod.conf.js
    | -- webpack.test.conf.js
| -- config
    | -- dev.env.js
    | -- index.js
    | -- prod.env.js
    | -- test.env.js
...
...

2.從npm run dev開始

本地調試一般需要熱重載,創建本地服務器,所以我們常使用的命令是npm run dev,根據package.json得知實際上執行的是node build/dev-server.js命令。

dev-server.js

好吧,讓我們看看dev-server.js到底做了什麼。

說實話做的就是,利用express創建一個服務,監聽特定的端口,利用express().use()使用wepack的中間件。可以改一改代理,是從本地localhost獲取數據改爲從其他代理地址獲取數據。

// 這是一個版本檢測腳本,主要是測試本地的npm和node版本是否要求
require('./check-versions')()

// ./check-versions.js
// 引入三個包,chalk(給字符串在命令行添加顏色)、semver(處理版本號字符串),child_process(添加個子進程)
var chalk = require('chalk')
var semver = require('semver')

// 引入package.json中的node和npm要求版本
var packageConfig = require('../package.json')

// child_process.execSync(cmd) 相同於同步在命令行執行cmd命令
function exec (cmd) {
  return require('child_process').execSync(cmd).toString().trim()
}

// 把信息都放入對象
var versionRequirements = [
  {
    name: 'node',
    currentVersion: semver.clean(process.version),
    versionRequirement: packageConfig.engines.node
  },
  {
    name: 'npm',
    currentVersion: exec('npm --version'),
    versionRequirement: packageConfig.engines.npm
  }
]

// 輸出一個函數
module.exports = function () {
  var warnings = []
  for (var i = 0; i < versionRequirements.length; i++) {
    var mod = versionRequirements[i]

    // semver工具比較版本是否符合
    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {

    // 不符合,則添加到warning數組
      warnings.push(mod.name + ': ' +
        chalk.red(mod.currentVersion) + ' should be ' +
        chalk.green(mod.versionRequirement)
      )
    }
  }

    // warning不爲空,命令界面輸出warning
  if (warnings.length) {
    console.log('')
    console.log(chalk.yellow('To use this template, you must update following to modules:'))
    console.log()
    for (var i = 0; i < warnings.length; i++) {
      var warning = warnings[i]
      console.log('  ' + warning)
    }
    console.log()

    // 強制終結所有相關進程(也就是說如果版本號不符合,直接跳出進程)
    process.exit(1)
  }
}

接下來繼續看幹了什麼:

// 引入opn包(利用默認瀏覽器打開uri路徑),express(web框架)
var opn = require('opn')
var express = require('express')

// 搭建本地的服務器
var app = express()

// 引入config配置對象
var config = require('../config')

// 進程沒有NODE_ENV的話,設置("production";"develpoment"或"testing")
if (!process.env.NODE_ENV) {
  process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}

// 端口號,沒有默認值則使用配置值(8080)
var port = process.env.PORT || config.dev.port

// config配置對象裏默認爲true
var autoOpenBrowser = !!config.dev.autoOpenBrowser

// app監聽端口,成功後由回調(是否打開網頁)
module.exports = app.listen(port, function (err) {
  if (err) {
    console.log(err)
    return
  }

  // when env is testing, don't need open it
  if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
    opn(uri)
  }
})

好了,現在express()監聽了端口,也可以自動打開了,但是裏面沒內容啊,而且熱重載之類的是怎麼做到的呢?

// 本地熱重載的url路徑
var uri = 'http://localhost:' + port

// 根據process.env.NODE_ENV選擇加載不同的webpack配置
var webpackConfig = process.env.NODE_ENV === 'testing'
  ? require('./webpack.prod.conf')
  : require('./webpack.dev.conf')

var webpack = require('webpack')  
var compiler = webpack(webpackConfig)

// webpack-dev-middler是爲webpack準備的中間件,服務webpack在連接服務器上導出的文件,開發專用
// 只寫在內存中,而不會佔用硬盤空間
// 如果文件發生改動,該中間件不再服務舊的打包文件,反而延遲請求直到編譯結束。所以你沒必要在因文件改動而刷新頁面前等待。
// Usage: app.use(webpackMiddleWare(webpack({}))
var devMiddleware = require('webpack-dev-middleware')(compiler, {
  publicPath: webpackConfig.output.publicPath,
  quiet: true
})

app.use(devMiddleware)

// 編譯結束後回調
devMiddleware.waitUntilValid(function () {
  console.log('> Listening at ' + uri + '\n')
})

// 這個模塊只和連接客戶端與webpack服務器以及接受更新相關。它會接受服務器的更新然後執行這些更新。
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
  log: () => {}
})

app.use(hotMiddleware)

// 當html-webpack-plugin模板發生改變的時候,強制熱加載
compiler.plugin('compilation', function (compilation) {
  compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
    hotMiddleware.publish({ action: 'reload' })
    cb()
  })
})

// 靜態文件的輸出路徑,這裏是"/static"
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)

// 如果希望所有通過express.static訪問的文件都存放在一個虛擬目錄下面,可以通過爲靜態資源目錄制定一個掛載路徑的方式來實現,如下:
app.use(staticPath, express.static('./static'))

還有一些。

// 單線程的代理中間件
var proxyMiddleware = require('http-proxy-middleware')

// 引入代理表,這裏爲空對象
var proxyTable = config.dev.proxyTable

/**
* 關於這個模塊的用法
* var express = require('express');
* var proxy = require('http-proxy-middleware');
* 
* var app = express()
*
* app.user('/api', proxy({target: 'http://www.example.org', changeOrigin: true}));
* app.listen(3000);
* http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar
* 上面這個意思是,/api這個請求會被中間件導向目標host,支持正則表達
*/
Object.keys(proxyTable).forEach(function (context) {
  var options = proxyTable[context]
  if (typeof options === 'string') {
    options = { target: options }
  }
  // 所有請求都將被導向新的host
  app.use(proxyMiddleware(options.filter || context, options))
})

// 針對單頁面應用的H5歷史API回調
// 單頁面應用通常只使用一個index文件作爲html文件,所以正常情況下後退瀏覽器會發生404。
app.use(require('connect-history-api-fallback')())

3.看一看webpack.conf配置

先了解一下merge模塊。

var merge = require('webpack-merge')
module.exports = merge(baseWebpackConfig, {})

utils.js

再看一下工具類裏面的函數。

// 這個模塊的作用是把文本從bundle中提取出來放入一個單獨的文件中
// 用法
// const ExtractTextPlugin = require("extract-text-webpack-plugin");

// module.exports = {
//  module: {
//    rules: [
//      {
//        test: /\.css$/,
//        use: ExtractTextPlugin.extract({
//          fallback: "style-loader",
//          use: "css-loader"
//        })
//      }
//    ]
//  },
//    plugins: [
//      new ExtractTextPlugin("styles.css"),
//    ]
// }
// 上面這個的意思是把所有的*.css模塊提取出來放到一個單獨的文件中(styles.css),css打包和js打包是相互獨立的
var ExtractTextPlugin = require('extract-text-webpack-plugin')

// 返回一個資源路徑字符串,子目錄下的路徑
exports.assetsPath = function (_path) {
  var assetsSubDirectory = process.env.NODE_ENV === 'production'
    ? config.build.assetsSubDirectory
    : config.dev.assetsSubDirectory
  return path.posix.join(assetsSubDirectory, _path)
}

/**
*** @
***
***
***
***
***
***
***
*** @return: {
***     css: [
                'vue-style-loader', 
                [
                    {
                        loader: 'css-loader',
                        options: {
                            minimize: process.env.NODE_ENV === 'production',
                            sourceMap: options.sourceMap
                        }
                    }
                ]
             ]
*** }
*** 
*/
exports.cssLoaders = function (options) {
  options = options || {}

  var cssLoader = {
    loader: 'css-loader',
    options: {
      minimize: process.env.NODE_ENV === 'production',
      sourceMap: options.sourceMap
    }
  }

  // generate loader string to be used with extract text plugin
  function generateLoaders (loader, loaderOptions) {
    var loaders = [cssLoader]
    if (loader) {
      loaders.push({
        loader: loader + '-loader',
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap
        })
      })
    }

    // Extract CSS when that option is specified
    // (which is the case during production build)
    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')
  }
}

// Generate loaders for standalone style files (outside of .vue)
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
}

webpack.base.conf

var path = require('path')

// 工具類
var utils = require('./utils')
var config = require('../config')

// vue-loader的options配置
var vueLoaderConfig = require('./vue-loader.conf')

// 返回dir的絕對路徑位置
function resolve (dir) {
  return path.join(__dirname, '..', dir)
}

上面定義了一些工具,具體配置如下:

const webpackConfig = {

  // 入口文件,webpack從這個文件開始打包所有相關文件
  entry: {
    app: './src/apps/index.js'
  },

  // 輸出路徑
  // 這裏path: '../dist'
  // publicPath: '/'
  // '../dist/'
  output: {
    path: config.build.assetsRoot,
    filename: '[name].js',
    publicPath: process.env.NODE_ENV === 'production'
      ? config.build.assetsPublicPath
      : config.dev.assetsPublicPath
  },

  // extensions,擴展名是這些的文件,引用的時候可以省略
  // alias,別名
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
      'apps': path.resolve(__dirname, '../src/apps'),
      'common': path.resolve(__dirname, '../src/common'),
    }
  },

  // 模塊規則,webpack可把其他資源(其他格式的文件)轉換爲js,利用的就是loader
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: vueLoaderConfig
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [resolve('src'), resolve('test')]
      },
      {
        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]')
        }
      }
    ]
  }
}

webpack.dev.conf

開發的配置主要是基礎配置再合併了特殊的開發配置對象(主要是插件)。

// 這個webpack插件簡單得創建了一些HTML文件來服務bundles
var HtmlWebpackPlugin = require('html-webpack-plugin')
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')

module.exports = merge(baseWebpackConfig, {

    //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
    //}

  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
  },

  devtool: '#cheap-module-eval-source-map',
  plugins: [
    new webpack.DefinePlugin({
      'process.env': config.dev.env
    }),

    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin(),

    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true,
      chunks: ['app']
    }),
    new FriendlyErrorsPlugin()
  ]
})

webpack.prod.conf

var CopyWebpackPlugin = require('copy-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')

// 自動在webpack構建的過程中搜索css資源並壓縮優化它們
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')

webpack.test.conf

3.npm run build

運行這行命令其實就是 node build/build.js

build.js

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

process.env.NODE_ENV = 'production'

// ora模塊就是在終端顯示優雅的旋轉等待符號
var ora = require('ora')
var spinner = ora('building for production...')
spinner.start()
spinner.stop()
var rm = require('rimraf')

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
    }) + '\n\n')

    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'
    ))
  })
})
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章