【vue系列之二】详解vue-cli 2.0配置文件

上次给大家分享的是用vue-cli 快速搭建 vue 项目,虽然很省时间和精力,但想要真正搞明白,我们还需要对其原理一探究竟。

大家拿到一个项目,要快速上手,正确的思路是这样的:

首先,如果在项目有 readme.md 的情况下,大家要先读 readme,项目的一些基本介绍,包括项目信息、运行的脚本、采用何种框架,以及项目维护者等信息通常都会有。一般在 git 上维护的项目都会有 readme.md,不熟悉 markdown 语法的同学可以先了解下markdown 入门

第二步,要看 package.json。现代的前端项目中通常都会有 package.json 文件。在 package.json 里,会介绍项目名称、版本、描述、作者、脚本、依赖包,对环境的要求,以及对浏览器要求。

 1 {
 2   "name": "uccn",
 3   "version": "1.0.0",
 4   "description": "uccn3.0",
 5   "author": "v_yangtianjiao <v_yangtianjiao@baidu.com>",
 6   "private": true,
   // 这里的脚本是分析项目的主要入口
7 "scripts": { 8 "dev": "node build/dev-server.js", 9 "start": "node build/dev-server.js", 10 "build": "node build/build.js", 11 "jsonp": "node build/jsonp-server.js" 12 },
   // 项目依赖
13 "dependencies": { 14 "fetch-jsonp": "^1.1.3", 15 "less": "^2.7.2", 16 "less-loader": "^4.0.4", 17 "stylus": "^0.54.5", 18 "stylus-loader": "^3.0.1", 19 "vue": "^2.4.2" 20 }, 21 "devDependencies": { 22 "autoprefixer": "^7.1.2", 23 "babel-core": "^6.22.1", 24 "babel-loader": "^7.1.1", 25 "babel-plugin-component": "^0.10.1", 26 "babel-plugin-transform-runtime": "^6.22.0", 27 "babel-preset-env": "^1.3.2", 28 "babel-preset-es2015": "^6.24.1", 29 "babel-preset-stage-2": "^6.22.0", 30 "babel-register": "^6.22.0", 31 "chalk": "^2.0.1", 32 "connect-history-api-fallback": "^1.3.0", 33 "copy-webpack-plugin": "^4.0.1", 34 "css-loader": "^0.28.0", 35 "cssnano": "^3.10.0", 36 "eventsource-polyfill": "^0.9.6", 37 "express": "^4.14.1", 38 "extract-text-webpack-plugin": "^2.0.0", 39 "file-loader": "^0.11.1", 40 "friendly-errors-webpack-plugin": "^1.1.3", 41 "html-webpack-plugin": "^2.28.0", 42 "http-proxy-middleware": "^0.17.3", 43 "opn": "^5.1.0", 44 "optimize-css-assets-webpack-plugin": "^2.0.0", 45 "ora": "^1.2.0", 46 "rimraf": "^2.6.0", 47 "semver": "^5.3.0", 48 "shelljs": "^0.7.6", 49 "url-loader": "^0.5.8", 50 "vue-loader": "^13.0.4", 51 "vue-style-loader": "^3.0.1", 52 "vue-template-compiler": "^2.4.2", 53 "webpack": "^2.6.1", 54 "webpack-bundle-analyzer": "^2.2.1", 55 "webpack-dev-middleware": "^1.10.0", 56 "webpack-hot-middleware": "^2.18.0", 57 "webpack-merge": "^4.1.0" 58 },
// 对 node 版本的以及 npm 版本的要求
59 "engines": { 60 "node": ">= 4.0.0", 61 "npm": ">= 3.0.0" 62 },
   // 浏览器要求,vue 项目不支持 ie8,因为 ie8 是 es3,尚没有 Object.defineProperty 属性
63 "browserslist": [ 64 "> 1%", 65 "last 2 versions", 66 "not ie <= 8" 67 ] 68 }

上面的 package.json 是从实际 vue 项目中摘出来的,大家从 package.json 中就会对项目有一个大概的了解,最主要的是脚本部分。通过 npm 的自动化任务,可以很方便的执行配置文件中的脚本。通过配置  "jsonp": "node build/jsonp-server.js",可以方便的使用 npm run jsonp 命令,代替 node build/jsonp-server.js 或者更复杂的一系列命令。详细的 npm 自动化命令可以移步npm 自动化

 

 现在的项目目录结构如上,我们从刚才的脚本入手。首先是启服务的脚本 npm run dev,实际上是执行 node build/dev-server.js,我们在 build 文件夹中找到 dev-server.js, 一步步分析。

/* eslint-disable */
// 首先检查 node 和 npm 的版本 require('./check-versions')()
// 获取配置文件中默认的配置
var config = require('../config')
// 如果 node 无法判断当前是开发环境还是生产环境,则使用 config.dev.env.NODE_ENV 作为当前的环境
if (!process.env.NODE_ENV) { process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) }

var opn = require('opn')// 用来在起来服务之后,打开浏览器并跳转指定 URL
var path = require('path')// node 自带文件路径工具
var express = require('express')// node 框架 express(本地开发的核心,起服务)
var webpack = require('webpack')// webpack, 压缩打包
var proxyMiddleware = require('http-proxy-middleware')// 中间件
var webpackConfig = require('./webpack.dev.conf')// 开发环境的 webpack 配置
var mockMiddleware = require('../config/dev.mock')// 开发环境本地 mock 数据中间件

var port = process.env.PORT || config.dev.port
var autoOpenBrowser = !!config.dev.autoOpenBrowser
var proxyTable = config.dev.proxyTable

var app = express()// 起服务
var compiler = webpack(webpackConfig)// webpack 进行编译
// webpack-dev-middleware 将编译的文件放在内存中,后续注入
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet:
true
})
// 热加载
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
log:
false,
heartbeat:
2000
})

compiler.plugin('compilation', function (compilation) {
compilation.plugin(
'html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action:
'reload' })
cb()
})
})

// proxy api requests
// proxyTable 中的配置挂载到 express 中

Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options
= {target: options}
}
app.use(proxyMiddleware(options.filter
|| context, options))
})

// 处理后退的时候匹配资源
app.use(require('connect-history-api-fallback')())

// 暂存在内存的 webpack 编译后的文件挂载到 express 上
app.use(devMiddleware)
// 将本地 mock 中间件挂载到 express 上
app.use(mockMiddleware);

// 热加载挂载到 express 上

app.use(hotMiddleware)

// 拼 static 静态资源文件路径
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
// express 为静态资源提供服务
app.use(staticPath, express.static(
'./static'))

var uri = 'http://localhost:' + port

var _resolve
var readyPromise = new Promise(resolve => {
_resolve
= resolve
})

console.log('> Starting dev server...')
devMiddleware.waitUntilValid(()
=> {
console.log(
'> Listening at' + uri + '\n')
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
opn(uri)
}
_resolve()
})
// 通过配置的端口,自动打开浏览器,并跳转拼好的 URL,至此,发开环境已经跑起来了
var server = app.listen(port)

module.exports = {
ready: readyPromise,
close: ()
=> {
server.close()
}
}

在上面的 dev-server 中,有很多变量来自于./config/index.js 和 webpack.dev.conf.js,我们一个个看上述配置文件。

首先看./config/index.js, 这里是整个项目主要的配置入口,我们在代码中一步步分析:

// node 自带路径工具.
var path = require('path')
// 分为两种环境,dev 和 production
module.exports = {
  build: {
    env: require('./prod.env'),// 使用 config/prod.env.js 中定义的编译环境
    index: path.resolve(__dirname, '../dist/index.html'),// 编译输入的 index.html 文件。node.js 中,在任何模块文件内部,可以使用 __filename 变量获取当前模块文件的带有完整绝对路径的文件名,
    assetsRoot: path.resolve(__dirname, '../dist'),// 编译输出的静态资源路径
    assetsSubDirectory: 'static',// 编译输出的二级目录
    assetsPublicPath: './', // 编译发布的根目录,可配置为资源服务器或者 cdn 域名
    productionSourceMap: false,// 是否开启 cssSourceMap
    productionGzip: false,// 是否开启 gzip
    productionGzipExtensions: ['js', 'css'],// 需要用 gzip 压缩的文件扩展名
    bundleAnalyzerReport: process.env.npm_config_report
  },
  dev: {
    env: require('./dev.env'),
    port: 8989,// 起服务的端口
    autoOpenBrowser: true,
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {},// 需要代理的接口,可以跨域
    cssSourceMap: false
  }
}

接着我们分析 webpack.dev.conf.js:

var utils = require('./utils')// 工具类
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')// 使用 webpack 配置合并插件
var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin')// 这个插件自动生成 HTML,并注入到.html 文件中
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')

// 将 hot-reload 相对路径添加到 webpack.base.conf 的对应的 entry 前面
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
baseWebpackConfig.entry[name]
= ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})
// webpack.dev.conf.js 与 webpack.base.conf.js 中的配置合并
module.exports
= merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({sourceMap: config.dev.cssSourceMap})
},
// webpack-devtool 有 7 种模式,cheap-module-eval-source-map 模式是比较快的开发模式
 

  devtool: '#cheap-module-eval-source-map',
  plugins: [
  // 你可以理解为,通过配置了 DefinePlugin,那么这里面的标识就相当于全局变量,你的业务代码可以直接使用配置的标识。
new webpack.DefinePlugin({ 'process.env': config.dev.env }), // hotModule 插件让页面变动时,只重绘对应的模块,不会重绘整个 HTML 文件 new webpack.HotModuleReplacementPlugin(),
  // 在编译出现错误时,使用 NoEmitOnErrorsPlugin 来跳过输出阶段。这样可以确保输出资源不会包含错误
new webpack.NoEmitOnErrorsPlugin(), // 将生成的 HTML 代码注入 index.html 文件 new HtmlWebpackPlugin({ filename: 'index.html', template: 'index.html', inject: true }),
  // friendly-errors-webpack-plugin 用于更友好地输出 webpack 的警告、错误等信息
new FriendlyErrorsPlugin()]})

 刚才的 webpack.dev.conf.js 中有引到 webpack.base.conf.js, 我们就把他们一网打尽,继续看 webpack.base.conf.js!

/* eslint-disable */
var path = require('path')// node 自带的文件路径插件
var utils = require('./utils')// 工具类
var config = require('../config')// 上面说过的 config/index
var vueLoaderConfig = require('./vue-loader.conf')// vue-loader.conf 配置文件是用来解决各种 css 文件的,定义了诸如 css,less,sass 之类的和样式有关的 loader
// 此函数是用来返回当前目录的平行目录的路径,
function resolve (dir) {
  return path.join(__dirname, '..', dir)
}

module.exports = {
entry: {
uccn:
'./src/main.js'// 入口
},
output: {
  // 路径是 config 目录下的 index.js 中的 build 配置中的 assetsRoot,也就是 dist 目录
path: config.build.assetsRoot,
filename:
'[name].js',
  // 上线地址,也就是真正的文件引用路径,如果是 production 生产环境,其实这里都是 '/'
publicPath: process.env.NODE_ENV
=== 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
 // resolve 是 webpack 的内置选项,顾名思义,决定要做的事情,也就是说当使用 import "jquery",该如何去执行这件事情,就是 resolve 配置项要做的,import jQuery from "./additional/dist/js/jquery" 这样会很麻烦,可以起个别名简化操作
resolve: {
  // 省略扩展名,比方说 import index form '../js/index', 会默认去找 index 文件,然后找 index.js,.vue,.josn.
extensions: [
'.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
    // 使用上面的 resolve 函数,意思是用 @代替 src 的绝对路径
'@': resolve('src'),
}
},
 // 不同的模块使用不同的 loader
module: {
rules: [
{
     // 对 vue 文件,使用 vue-loader 解析
test:
/.vue$/,
loader:
'vue-loader',
options: vueLoaderConfig
},
{
     // babel-loader 把 es6 解析成 es5
test:
/.js$/,
loader:
'babel-loader',
include: [resolve(
'src'), resolve('test')]
},
{
     // url-loader 将文件大小低于下面 option 中 limit 的图片,转化为一个 64 位的 DataURL,这样会省去很多请求,大于 limit 的,按 [name].[hash:7].[ext] 的命名方式放到了 static/img 下面,方便做 cache
test:
/.(png|jpe?g|gif|svg)(?.)?$/,
loader:
'url-loader',
options: {
limit:
20000,
name: utils.assetsPath(
'img/[name].[hash:7].[ext]')
}
},
{
     // 音频和视频文件处理,同上
test:
/.(mp4|webm|ogg|mp3|wav|flac|aac)(?.
)?$/,
loader:
'url-loader',
options: {
limit:
10000,
name: utils.assetsPath(
'media/[name].[hash:7].[ext]')
}
},
{
     // 字体处理,同上 
test:
/.(woff2?|eot|ttf|otf)(?.*)?$/,
loader:
'url-loader',
options: {
limit:
10000,
name: utils.assetsPath(
'fonts/[name].[hash:7].[ext]')
}
}
]
}
}

 至此,npm run dev起本地开发环境相关的配置文件基本说完了,接着说一下上面都用到的 util 工具类:

var path = require('path')
var config = require('../config')
// extract-text-webpack-plugin 该插件的主要是为了抽离 css 样式, 防止将样式打包在 js 中引起页面样式加载错乱的现象
var ExtractTextPlugin = require('extract-text-webpack-plugin')
// 返回资源文件路径,path.posix 以 posix 兼容的方式交互,是跨平台的,如果是 path.win32 的话,只能在 win 上 exports.assetsPath
= function (_path) { var assetsSubDirectory = process.env.NODE_ENV === 'production' ? config.build.assetsSubDirectory : config.dev.assetsSubDirectory return path.posix.join(assetsSubDirectory, _path) }
// 通过判断是否是生产环境,配置不同的样式语言的 loader 配置 exports.cssLoaders
= function (options) { options = options || {}

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

// 生成各种 loader 配置,通过传入不同的 loader 和 option,将不同样式文件语言的 loader 拼好,push 到 loader 配置中。
function generateLoaders (loader, loaderOptions) {
var loaders = [cssLoader]
if (loader) {
loaders.push({
loader: loader
+ '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}

</span><span style="color: rgba(255, 0, 0, 1)">// extract-text-webpack-plugin有三个参数,use指需要用什么loader去编译文件;fallback指编译后用什么loader去提取文件;还有一个publicfile用来覆盖项目路径</span>
<span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (options.extract) {
  </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> ExtractTextPlugin.extract({
    use: loaders,
    fallback: </span>'vue-style-loader'<span style="color: rgba(0, 0, 0, 1)">
  })
} </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
  </span><span style="color: rgba(0, 0, 255, 1)">return</span> ['vue-style-loader'<span style="color: rgba(0, 0, 0, 1)">].concat(loaders)
}

}

// 对不同的样式语言,返回相应的 loader
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders(
'less'),
sass: generateLoaders(
'sass', { indentedSyntax: true }),
scss: generateLoaders(
'sass'),
stylus: generateLoaders(
'stylus'),
styl: generateLoaders(
'stylus')
}
}

// 生成处理不同的样式文件处理规则
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
}

———————————————— 华丽的分隔符 —————————————————

下面我们继续说 npm run build,打包编译的一系列操作 ~

从 package.json 中可以看出,npm run build,其实是执行了 node build/build.js,我们在 build 文件夹中找到 build.js,build 主要的工作是:检测 node 和 npm 版本,删除 dist 包,webpack 构建打包,在终端输出构建信息并结束,如果报错,则输出报错信息。

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

process.env.NODE_ENV = 'production'
// 在终端显示的旋转器插件
var ora = require('ora')
// 用于删除文件夹
var rm = require('rimraf')
var path = require('path')
// 终端文字颜色插件
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('../config')
var webpackConfig = require('./webpack.prod.conf')

var spinner = ora('building for production...')
spinner.start()
// 删除 dist 文件夹,之后 webpack 打包
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')

</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (stats.hasErrors()) {
  console.log(chalk.red(</span>'  Build failed with errors.\n'<span style="color: rgba(0, 0, 0, 1)">))
  process.exit(</span>1<span style="color: rgba(0, 0, 0, 1)">)
}

console.log(chalk.cyan(</span>'  Build complete.\n'<span style="color: rgba(0, 0, 0, 1)">))
console.log(chalk.yellow(
  </span>'  Tip: built files are meant to be served over an HTTP server.\n' +
  '  Opening index.html over file:// won\'t work.\n'<span style="color: rgba(0, 0, 0, 1)">
))

})
})

build.js 用到了 webpack.prod.conf.js, 他与 webpack.base.conf.js merge 之后,作为 webpack 配置文件,我们再看看 webpack.prod.conf.js, 主要做的工作是:
1. 提取 webpack 生成的 bundle 中的文本,到特定的文件,使得 css,js 文件与 webpack 输出的 bundle 分离。

2. 合并基本的 webpack 配置

3. 配置 webpack 的输出,包括输出路径,文件名格式。

4. 配置 webpack 插件,包括丑化代码。

5.gzip 下引入 compression 插件进行压缩。

/* eslint-disable */
var path = require('path')
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var CopyWebpackPlugin = require('copy-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
// 用于从 webpack 生成的 bundle 中提取文本到特定文件中的插件
// 可以抽取出 css,js 文件将其与 webpack 输出的 bundle 分离
var ExtractTextPlugin = require('extract-text-webpack-plugin') var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')

var env = config.build.env
// 合并基础的 webpack 配置
var webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract:
true
})
},
 // 7 中 sourceMap 上面有讲过
devtool: config.build.productionSourceMap
? '#source-map' : false,
 // 配置 webpack 输出的目录,及文件命名规则
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath(
'js/[name].min.js'),
chunkFilename: utils.assetsPath(
'js/[id].[chunkhash].js')
},
 // webpack 插件配置
plugins: [
// 同 webpack.dev.conf.js
new webpack.DefinePlugin({
'process.env': env
}),
  // 丑化代码
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings:
false
},
sourceMap:
true
}),
// 抽离 css 文件到单独的文件
new ExtractTextPlugin({
filename: utils.assetsPath(
'css/[name].min.css')
}),

new OptimizeCSSPlugin({
cssProcessorOptions: {
safe:
true
}
}),
// 生成并注入 index.html
new HtmlWebpackPlugin({
filename: config.build.index,
template:
'index.html',
inject:
true,
minify: {
removeComments:
true,
collapseWhitespace:
false,
removeAttributeQuotes:
true
},
chunksSortMode: 'dependency'
}),
// keep module.id stable when vender modules does not change
new webpack.HashedModuleIdsPlugin(),
split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name:
'vendor',
minChunks:
function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource
&&
/.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname,
'../node_modules')
)
=== 0
)
}
}),
extract webpack runtime and module manifest to its own file
in order to
prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name:
'manifest',
chunks: [
'vendor']
}),
copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname,
'../static'),
to: config.build.assetsSubDirectory,
ignore: [
'.*']
}
])
]
})
// gzip 模式下需要引入 compression 插件进行压缩
if (config.build.productionGzip) {
var 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,
minRatio:
0.8
})
)
}

if (config.build.bundleAnalyzerReport) {
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(
new BundleAnalyzerPlugin())
}

module.exports = webpackConfig

到此为止,vue 官方脚手架工具 vue-cli 2.0 的所有配置文件都已介绍完毕,从头到尾再梳理一遍:

执行 npm run dev 或者 npm run start,实际是在 node 环境执行 build/dev-server.js, dev-server.js 会去拿到 config 中的端口等配置,通过 express 起一个服务,通过插件自动打开浏览器,加载 webpack 编译后放在内存的 bundle。

执行 npm run build,实际上执行了 build/build.js, 通过 webpack 的一系列配置及插件,将文件打包合并丑化,并创建 dist 目录,放置编译打包后的文件,这将是未来用在生产环境的包。

 

写这篇文章我自身的收获也挺多,第一是对 vue-cli 整体的认知更加清晰条理,第二是对 webpack 的一些插件有了新的认识。以前对一些插件模棱两可,直接越过,这是不对的,要一步一个脚印儿,遇坑填坑,这样才会有收获。虽然过程可能是艰辛的,但收获将会是巨大的 ~

文章中不足之处希望大家多多指正!

参考文献:

extract-text-webpack-plugin 的使用及安装

vue-cli 的 webpack 模板项目配置文件分析

webpack——devtool 里的 7 种 SourceMap 模式

vue-cli#2.0 webpack 配置分析

__dirname 与 __filename