从vue-cli中看node.js和webpack的运作

鲜于璞瑜
2023-12-01

前言

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'
    ))
  })
})
 类似资料: