详解vue-cli 2.0配置文件(小结)
上次给大家分享的是用,虽然很省时间和精力,但想要真正搞明白,我们还需要对其原理一探究竟。
大家拿到一个项目,要快速上手,正确的思路是这样的:
首先,如果在项目有readme.md的情况下,大家要先读readme,项目的一些基本介绍,包括项目信息、运行的脚本、采用何种框架,以及项目维护者等信息通常都会有。一般在git上维护的项目都会有readme.md,不熟悉markdown语法的同学可以先了解下。
第二步,要看package.json。现代的前端项目中通常都会有package.json文件。在package.json里,会介绍项目名称、版本、描述、作者、脚本、依赖包,对环境的要求,以及对浏览器要求。
{ "name": "uccn", "version": "1.0.0", "description": "uccn3.0", "author": "v_yangtianjiao <v_yangtianjiao@baidu.com>", "private": true, // 这里的脚本是分析项目的主要入口 "scripts": { "dev": "node build/dev-server.js", "start": "node build/dev-server.js", "build": "node build/build.js", "jsonp": "node build/jsonp-server.js" }, // 项目依赖 "dependencies": { "fetch-jsonp": "^1.1.3", "less": "^2.7.2", "less-loader": "^4.0.4", "stylus": "^0.54.5", "stylus-loader": "^3.0.1", "vue": "^2.4.2" }, "devdependencies": { "autoprefixer": "^7.1.2", "babel-core": "^6.22.1", "babel-loader": "^7.1.1", "babel-plugin-component": "^0.10.1", "babel-plugin-transform-runtime": "^6.22.0", "babel-preset-env": "^1.3.2", "babel-preset-es2015": "^6.24.1", "babel-preset-stage-2": "^6.22.0", "babel-register": "^6.22.0", "chalk": "^2.0.1", "connect-history-api-fallback": "^1.3.0", "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.0", "cssnano": "^3.10.0", "eventsource-polyfill": "^0.9.6", "express": "^4.14.1", "extract-text-webpack-plugin": "^2.0.0", "file-loader": "^0.11.1", "friendly-errors-webpack-plugin": "^1.1.3", "html-webpack-plugin": "^2.28.0", "http-proxy-middleware": "^0.17.3", "opn": "^5.1.0", "optimize-css-assets-webpack-plugin": "^2.0.0", "ora": "^1.2.0", "rimraf": "^2.6.0", "semver": "^5.3.0", "shelljs": "^0.7.6", "url-loader": "^0.5.8", "vue-loader": "^13.0.4", "vue-style-loader": "^3.0.1", "vue-template-compiler": "^2.4.2", "webpack": "^2.6.1", "webpack-bundle-analyzer": "^2.2.1", "webpack-dev-middleware": "^1.10.0", "webpack-hot-middleware": "^2.18.0", "webpack-merge": "^4.1.0" }, // 对node版本的以及npm版本的要求 "engines": { "node": ">= 4.0.0", "npm": ">= 3.0.0" }, // 浏览器要求,vue项目不支持ie8,因为ie8是es3,尚没有object.defineproperty属性 "browserslist": [ "> 1%", "last 2 versions", "not ie <= 8" ] }
上面的package.json是从实际vue项目中摘出来的,大家从package.json中就会对项目有一个大概的了解,最主要的是脚本部分。通过npm的自动化任务,可以很方便的执行配置文件中的脚本。通过配置 "jsonp": "node build/jsonp-server.js",可以方便的使用npm run jsonp命令,代替node build/jsonp-server.js或者更复杂的一系列命令。详细的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 }) }) } // extract-text-webpack-plugin有三个参数,use指需要用什么loader去编译文件;fallback指编译后用什么loader去提取文件;还有一个publicfile用来覆盖项目路径 if (options.extract) { return extracttextplugin.extract({ use: loaders, fallback: 'vue-style-loader' }) } else { return ['vue-style-loader'].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') 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用到了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的一些插件有了新的认识。以前对一些插件模棱两可,直接越过,这是不对的,要一步一个脚印儿,遇坑填坑,这样才会有收获。虽然过程可能是艰辛的,但收获将会是巨大的~希望对大家的学习有所帮助,也希望大家多多支持。
推荐阅读
-
详解vue-cli 2.0配置文件(小结)
-
详解vue2.0脚手架的webpack 配置文件分析
-
Spring Boot 配置文件详解(小结)
-
基于vue-cli 打包时抽离项目相关配置文件详解
-
详解vue2.0 使用动态组件实现 Tab 标签页切换效果(vue-cli)
-
详解vue-cli中的ESlint配置文件eslintrc.js
-
vue-cli脚手架build目录下utils.js工具配置文件详解
-
vue-cli中的babel配置文件.babelrc实例详解
-
详解vue-cli脚手架build目录中的dev-server.js配置文件
-
.NET Core 2.0迁移小技巧之web.config 配置文件示例详解