前言
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'
))
})
})