直接用vue-cli创建的项目可以创建一个单页面应用,开发环境和生产环境都是以一个单独的项目为目录的。在写一些有共性的模块时需要将所有组件放在同一个大的框架下的同时又需要每个模块可以进行单独的启动和打包。此时就需要进行个性化配置。
场景:需要src下不是一个单独的模块而是一组同一个大的项目的模块组。将单独的模块组放在文件夹module下,而将一些通用的通用css、通用js配置或方法、通用图片、以及vue组建等通用模块放在common中。此时构建的目录结构大致如下:
—build
—config
—node_modules
—src
——common
————js
————css
————pic
————components
——module
————head
—————index.html
—————src
———————assets
———————components
———————router
———————app.vue
———————main.js
————face
————hand
————body
————foot
——package
—package.json
需要对webpack打包文件进行配置,可以使用以下命令启动和打包单独的模块:
npm run dev <module name>
npm run build <module name>
对单独的每个模块进行开发和打包。例如:运行npm run dev demo
来对demo模块进行开发模式的启动。为实现以上目的,首先要看一下vue-cli自带的原配置。首先先查看原先的package.json来看看自带的配置都做了写什么。package.json中关于script的配置如下:
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"build": "node build/build.js"
}
关于package.json中的script配置:npm 允许在package.json文件里面,使用scripts字段定义脚本命令
也就是说相当于运行npm run命令时会按照package.json中的命令来进行脚本运行而无需自己在命令行中输入那么长的脚本命令。关于npm script想了解更多的话可以看看阮一峰老师的npm scripts 使用指南
首先对开发环境的dev命令运行进行配置更改,原配置中执行dev
实际上运行的是
webpack-dev-server --inline --progress --config build/webpack.dev.conf.js
这条命令启动webpack-dev-server
模块并将配置参数传入,关于配置参数--
是webpack声明参数的方式,在我们运行dev时传入的参数包括:
inline
: 布尔值,用于在 dev-server 的两种不同模式之间切换。默认情况下,应用程序启用内联模式(inline mode)。这意味着一段处理实时重载的脚本被插入到你的包(bundle)中,并且构建消息将会出现在浏览器控制台。也可以使用 iframe 模式,它在通知栏下面使用<iframe>
标签,包含了关于构建的消息。当inline的值为false时启用iframe模式。progess
:该配置只用于命令行工具,用于将运行进度输出到控制台。config
:配置信息,在此处传入build/webpack.dev.conf.js。运行由配置文件导出的函数,并且等待 Promise 返回。便于需要异步地加载所需的配置变量。
那么对于开发模式的修改就应当时从配置文件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')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
// 与webpack.base.conf进行合并
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool,
// these devServer options should be customized in /config/index.js
devServer: {
clientLogLevel: 'warning',
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
hot: true,
contentBase: false, // since we use CopyWebpackPlugin.
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, // necessary for FriendlyErrorsPlugin
watchOptions: {
poll: config.dev.poll,
}
},
plugins: [
// 允许在编译时(compile time)配置的全局常量
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
// 启用模块热替换
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
// 在输出阶段时,遇到编译错误跳过
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
// 简单创建 HTML 文件,用于服务器访问
new HtmlWebpackPlugin({
// 要将HTML写入的文件
filename: 'index.html',
// 本地模板文件的位置,支持加载器(如handlebars、ejs、undersore、html等)
template: 'index.html',
// 传递true或'body'所有javascript资源将被放置在body元素的底部。'head'将脚本放在head元素中
inject: true
}),
// copy custom static assets
// 将单个文件或整个目录复制到构建目录
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
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)
}
})
})
]
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')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
// 与webpack.base.conf进行合并
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool,
// these devServer options should be customized in /config/index.js
devServer: {
clientLogLevel: 'warning',
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
hot: true,
contentBase: false, // since we use CopyWebpackPlugin.
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, // necessary for FriendlyErrorsPlugin
watchOptions: {
poll: config.dev.poll,
}
},
plugins: [
// 允许在编译时(compile time)配置的全局常量
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
// 启用模块热替换
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
// 在输出阶段时,遇到编译错误跳过
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
// 简单创建 HTML 文件,用于服务器访问
new HtmlWebpackPlugin({
// 要将HTML写入的文件
filename: 'index.html',
// 本地模板文件的位置,支持加载器(如handlebars、ejs、undersore、html等)
template: 'index.html',
// 传递true或'body'所有javascript资源将被放置在body元素的底部。'head'将脚本放在head元素中
inject: true
}),
// copy custom static assets
// 将单个文件或整个目录复制到构建目录
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
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)
}
})
})
既然要在开发时每个模块要进行单独的开发那模块启动时应该根据传入的模块名进行启动,而不是固定的main.js.因此需要对入口文件进行改动。入口文件的配置在webpack.base.conf
中,配置如下:
entry: {
app: './src/main.js'
}
应该改为:
entry: {
app: './src/module/' + entrydir + '/src/main.js'
}
注意webpack.dev.conf.js中是没有entry配置的,这个文件直接调用了webpack.base.conf
的配置,因此可以直接添加进配置对象中。其中entryname是需要启动的模块项目名称。后续会进行详细说明。
当然了也可以直接改webpack.base.conf
的配置。但是从图中的merge函数其实是可以看出来更改dev配置会覆盖并添加上webpack.base.conf
的配置的
除了需要修改服务启动的入口文件目录以外,还有一个需要注意的地方,webpack.dev.conf.js中的生成html的插件HtmlWebpackPlugin中模版属性需要替换为模块内相应的index.html而不是根目录下的index.html
如果你的index.html没有进行更改那可能用哪个index.html文件作为模版都是一样的,但是如果改动了则一定要用当前项目的index.html作为生成html的模版。
其中entrydir为项目名,现在的问题是把项目名通过命令行传入配置文件中。接下来研究如何通过命令行获取目录名的问题。
关于命令行传入参数的方式,经过了许多尝试。
- 最开始使用的方式是
--key value
在命令行后面加上--name demo
结果会被提示demo这个模块没有安装。根据报错提示可以看出webpack并未将demo看成是name的值而是识别为一个单独的模块。在看了webpack-dev-server.js时发现该模块使用命令行工具yargs
模块进行参数读取而我们自定义的参数name
是不会进行识别读取的。会有一下报错:
- 第二个使用的方式是使用DefinePlugin插件自定义数据然后在命令行中进行参数定义。
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
})
可以看到自定义参数是托管在process.env中。此处自定义的参数配置定义在config/dev.env中。打开该文件发现参数如下:
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
name: ''
})
在参数NODE_ENV
下加上我们自定义的参数name
, 然后在命令行传入name,更改package.json如下:
"dev": "entryname=demo webpack-dev-server --inline --progress --config build/webpack.dev.conf.js"
使用后果然可以成功启动demo模块
然而使用这个方式有一个比较大的bug就是只能用
"dev": "entryname=demo webpack-dev-server --inline --progress --config build/webpack.dev.conf.js"
而不能使用
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js entryname=demo"
这就意味着不能使用
npm run dev entryname=demo
来启动,而如果试图将entryname=demo加在dev之前,则会
所以不符合我最初的npm run dev <some code>
的启动方式,pass掉
- 使用webpack自带的环境选项,当 webpack 配置对象导出为一个函数时,可以向起传入一个"环境对象"。例如:
webpack --env.production # 设置 env.production == true
webpack --env.platform=web # 设置 env.platform == "web"
关于–env的参数用法官方给出了以下示例:
关于webpack的命令行接口参数可以点击这里查看官方文档
按照官方文档对package.json进行更改
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js -- --env.name=demo"
因为传参数是在结尾传递的可以通过命令行传递
npm run dev -- --env.name=demo
不需要对dev进行更改也可以运行
再将 webpack.dev.conf.js代码进行更改,需要更改的部分是之前提到的entry和HtmlWebpackPlugin插件的template属性,命令行参数可以用argv进行参数接接收。关于process.argv官方描述如下:
process.argv 属性返回一个数组,这个数组包含了启动Node.js进程时的命令行参数。第一个元素为process.execPath。如果需要获取argv[0]的值请参见 process.argv0。第二个元素为当前执行的JavaScript文件路径。剩余的元素为其他命令行参数。
关于官方process.argv 的详细解释请点击这里
用如下语句获取模块名作为路径传给入口文件配置和index文件模版配置。更改方式上文已经提到了,这里不再赘述。
const entrydir = process.argv[process.argv.length - 1].replace(/^(\S)*=/, '')
运行结果如下:
效果不错是最初想要的npm run dev <some code>
启动开发模式的方式。但是要输入npm run dev -- --env.name=demo
这么长一串好像不太合理。最好是输入npm run demo
就可以启动。
那能不能配置
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js -- --env.name=",
然后运行npm run demo
,虽然看起来很美好但显然不能,因为有空格就会识别为两个部分,所以又想只输入模块名又要把模块名作为参数录入给命令行就只能通过另一个js文件来起到“中间人”的作用。具体方式是:
- 更改package.json
- 构建用来将参数传递给配置文件devServer,首先在build目录下创建一个js文件取一个你认为合适的名字用dev配置为启动的命令简写。devServer.js内容如下。
const cprocess = require('child_process')
let entryDir = process.argv[process.argv.length-1]
let cmd = 'npm run startdev -- --env.name=' + entryDir
let dev = cprocess.exec(cmd, {detached: true} ,function(error, stdout, stderr) {
if(error) console.log(error)
})
dev.stdout.pipe(process.stdout)
dev.stderr.pipe(process.stderr)
这里写的比较简单也可以用命令行工具作出更多样化的功能。这里实现的功能是接收参数开启子进程将参数拼接在子进程中运行,并将子进程的标准输出和标准错误与父进程的标准输出和标准错误通过管道进行链接,这样就可以及时的获取子进程的输出及报错信息了。关于node的子进程使用方式想了解更多的话可以点击这里
到这里开发模式的相关个性化配置就告一段落。下面来看看生产模式下的个性化配置, 同上先从package.json入手看看我们npm run build
之后到底发生了什么,scripts配置如下:
"build": "node build/build.js"
可以看到实际上运行build命令后会启动build下的文件build.js,那就来看看build.js
'use strict'
require('./check-versions')()
process.env.NODE_ENV = 'production'
const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')
// 出现加载标示
const spinner = ora('building for production...')
spinner.start()
// rimraf 包的作用:以包的形式包装rm -rf命令,用来删除文件和文件夹的,不管文件夹是否为空,都可删除
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, (err, stats) => {
// 加载标识结束
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
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干了这么几件事
- 进行版本检测不合适就报错
- 使用ora进行加载图标的渲染,打包完成就显示打包结果
- 把webpack.prod.conf配置传给webpack进行打包,并把相关结果传给标准输出打印在屏幕上。有错误报错误没错误,没错误报信息。
所以如果要进行打包修改重点是第三个更改配置文件。但是怀着求知的心态,也来看看前两点webpack是怎么做的,如果并不想了解可以往下浏览直接看webpack.prod.conf的配置即可:
- 版本检测的文件check-versions
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')
// 定义子进程exec并将返回进程的输出流
// execSync返回: <Buffer> | <string> 该命令的 stdout
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
const versionRequirements = [
{
name: 'node',
// 返回一个标准的版本号,且去掉两边的空格
currentVersion: semver.clean(process.version),
// 引入package.json中的engines.node 制定node版本的范围
versionRequirement: packageConfig.engines.node
}
]
// 检测是否存在npm的路径
// 如果有则在versionRequirements中添加npm相关信息对象
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
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]
// satisfies(version, range):如果版本满足范围,则返回true
// 判定当前版本是否满足版本范围, 不满足则将报错信息存储在warnings中
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)
}
}
- 关于org加载图标的问题主要用来实现一个命令行下的加载效果。大致用法如下:
const ora = require('ora');
const spinner = ora('Loading unicorns').start();
setTimeout(() => {
spinner.color = 'yellow';
spinner.text = 'Loading rainbows';
}, 1000);
以上命令1s后加载图标的颜色和内容会改变。但是关于ora更多的用法还是应该去看看官方的git,git传送门在此
- 当然最重要的部分还是得看看webpack.prod.conf配置,因为配置其实大体和webpack.dev.conf差不多所以没有再重新注释一遍的必要了,直接来说如何修改。
先来明确目的是为了将参数从命令行传入文件中,等等这个需求好像上文见过?没错这个和之前开发模式的需求相同所以需要更改的位置也一样分别是入口文件entry和HtmlWebpackPlugin的template。但是这里就不要第三个文件介入因为直接传参数默认就会传给你启动的这个文件。
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"build": "node build/build.js",
第一种情况需要把参数传给webpack.dev.js而不是启动的webpack-dev-server 因此需要加标示来声明这一点参数是传给配置文件的而不是启动的文件。第二个生产命令则不需要 因为命令本来就是传给build.js和该环境下启动的webpack及其配置文件的。只需要做好接收就好了。接收参数方式和之前一样。不再赘述。这里放运行结果图:
至此,配置已经基本完成了。后续可以引入命令行达到更好的体验。
别名配置:修改webpack.base.conf文件可以使得文件引用更加方便,而且@如果不对@进行修改则其目录指向为'@': resolve('src'),
指向的时项目根目录的src文件而不是单模块目录下的src,就会造成引用错误。