上一篇 《webpack基础使用》
前言:webpack的配置其实挺多,而且更多的是体现在loader和plugin方面的配置,上篇我们只是简单介绍webpack基础使用,因为我觉得更多细节方面可以在vue-cli生成的工程中学习到。大家现在用工具生成出来的是基于webpack 3x版本的,比2x版本的配置更简洁清晰,不同点是:2x版本的用了webpack-dev-middle和webpack-hot-middleware插件提供模块热更新,而3x版本的配置则是用webpack-dev-server;其实两者有好有坏,当然相比之下,我觉得webpack-dev-server会更直接点。
vue-cli的使用
//vue-cli工具很简单,命令行里:
npm install vue-cli -g
// 安装完之后,就有vue命令了
vue init webpack vue-webpack2 // 初始化一个webpack工程,工程名字为vue-webpack2
//下面图片中,eslint那一行选择了yes,eslint是用于管理代码格式的,良好的编码格式是很重要,unit test 和nigtht watch 就选择no,因为这次我们主要是研究webpack配置哈
命令执行完后,在当前目录下就会看到新的文件夹,也就是你的工程vue-webpack2(现在vue-cli出来的工程是基于webpack3的, 我给大家提供一个webpack2配置的版本,下一篇文章里再讲基于webpack3)。
目录结构:
src:放我们自己代码
build和config:webpack配置,我们学习的重点
其他配置文件:稍后讲
第一部分:非重点但注意的配置文件
.editorconfig文件
这个文件主要是对编辑器的编辑做设置,里面主要设置一个tab缩进多少个空格,换行符(linux系统的是lf, window系统则是ctlf),还有编码设置等。这个文件生效需要你安装editorconfig插件,这个插件支持众多ide编辑器,像sublime、vscode、eclipse,主要是为了统一编辑,使得我们的js能够运行到其他操作系统。(为什么java不需要,因为jvm虚拟机最终执行的java文件编译后的二进制.class文件,不同平台有不同jvm,所以java是跨平台的)
.eslintrc.js
eslint是用于统一团队之间的编码风格的工具,以前看过一些老代码,风格不统一,看起来很痛苦。有些人是分号党,有些人却不是。eslint对换行、空格等都可以配置一套规则,团队里基于这套规则写出的代码,在阅读性就做到了统一。大家可以参考https://eslint.org/了解其详细的配置。上手很容易,npm install eslint 然后,eslint src/main.js ,工具就根据.eslintrc.js配置开始检查main.js。这种用法比较初级,我们可以看一下我们的工程里是怎么使用的。
package.json
这个就不用说了吧,我们可以了解一下npm script的使用技巧,看下图
工程里给我们配置了四个任务,所以你就可以执行npm run dev 或者执行npm run build ,以及npm run lint。
比如 npm run lint ,实际执行的就是对应的: eslint --ext .js, .vue src 这个就是告诉eslint帮我们检查src下面的js文件和vue文件。另外你也可以添加配置:
"test": "npm run lint & npm run dev"
当你执行npm run test 就会执行npm run lint后再执行 npm run dev
--其他文件,下面讲webpack配置会讲到,好进入第二部分--
第二部分: webpack配置()
bulid目录下的webpack.base.conf.js
var path = require('path') // node path模块
var utils = require('./utils')
var config = require('../config') // config目录,vue-cli工程分成两个环境,一个是开发的dev环境,一个是生产环境production
var vueLoaderConfig = require('./vue-loader.conf') // 引入vue-loader的配置,vue-loader是处理.vue文件使用的
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src')
}
},
module: { // 定义对文件的处理loader
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',
options: {
limit: 10000,
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]')
}
}
]
}
}
这个文件定义webpack的基础配置:
引入config:对应config目录的index.js: 这个主要是为区分开发环境与生产环境的不同,比如开发环境是本机测试的,那么我的publicPath设置为空,但是生产环境则设置aliyu.cdn.com,所以output里面的publicPath根据process.env.NODE_ENV 是否为开发环境,引用config对应的属性。比如开发环境引入图片的url是<img src='/pic.jpg'>,而生产环境则是<img src='http://aliyun.cdn.com/pic.jpg'>。这就是output的publicPath的作用,设置config就是为了区分开发环境还是生产环境。
resolve:
extentions: 配置这个参数后,可以省略扩展名。如:import * from 'test.js' 可以写成 import * from 'test'
alias: 重命名。路径上的重命名,比如你要import一个模块,路径是D://project/vue-webapck2/src/modules/car 写完整路径很长很累,工程中配置了,我们可以写成 @/modules/car
下面则是对文件处理定义了loader:
loader是定义在module.rules中,其实意思就是对模块文件的处理规则。因为webpack把每个一个文件,哪怕是图片视频都当成一个模块,只不过它识别不了需要这loader处理工具来帮助它。
test:正则匹配,匹配.vue文件用vue-loader处理,.js用babel-loader处理
loader: 指定处理的loader工具
options: loader怎么处理文件也需要你设置参数,你可以用个options传递你设置的参数给它
bable-loader: 对js文件做处理,这样我们可以用es6、es7规范来写js,babel-loader会根据项目根目录下的.bablerc文件的配置对于你的js代码进行转义,有些浏览器没有实现es6 或者es7规范,所以这就是bable存在的意义。
url-loader:对资源文件做base64编码,它有个参数limit,比如一张图片小于这个limit的值,那url-loader会帮你转成base64编码嵌入引用这张图片的qit模块中,这样浏览器就不需要多一个网络请求,去请求图片,增加网页的响应时间。当然超过这个值的话,还是给你提供成url链接。工程中的配置,可以看到它对图片,视频,字体文件都可以转。
vue-loader:vue官方提供的对vue文件的处理,它会将vue文件中css的部分交由webpack指定的css-loader处理,js和模板交由webpack的js指定loader也就是babel-loader处理;处理具体配置不多讲,参考https://github.com/vuejs/vue-loader
~~~~~~~~~~~~~~分割线~~~~~~~~~~~~~~~~~~~
现在我们知道了,webpack.base.conf.js配置了基础性的配置,然后我们的配置暴露出去。那我们怎么使用它呢?首先我们知道package.json帮我们配置了build 和 dev 两个任务,它们分别对应执行的 npm run lint && node build/build.js 和 node build/dev-server.js。npm run build, 先执行eslint,帮忙lint一下代码的风格,然后执行node build/build.js 。那我们先看build.js
require('./check-versions')()
process.env.NODE_ENV = 'production'
var ora = require('ora') // 一个用于在命令窗口提示类似程序处理中,loading中之类文字,以起到提醒标注作用
var rm = require('rimraf') // rm 删除目录,清空目录的工具包
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...') // 命令窗口会出现一个loading转圈
spinner.start()
// rm 帮我们每次构建前,清理一下之前构建好的旧文件,清理完后执行回调函数
// 回调函数里执行webpack打包
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, function (err, stats) { //webpack打包后执行回调函数,向控制台输出自己构建结果信息
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'
))
})
})
webapck有两种使用方式:
第一种: 命令行里webpack --config src/main.js
第二种: 就是require('webpack'), 给webpack传入config配置对象,然后执行这段node脚本,即node build/build.js
然后我们就可以知道,webpack配置参数config是从webpack.prod.conf引入的:
webpack.prod.conf.js: 这里主要是用了webpack-merge 合并基础的配置,根据环境的不同,添加不同的配置。prod就是prodution生产环境。这里面用到了一些插件,具体我都注释到上面
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')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
var env = config.build.env
// 利用webpack-merge 合并我们的baseWebpackConfig配置。 webpack-merge能够让你动态改变webpack配置
var webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true
})
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
// 添加chunkhash值,指每次构建的值都不一样,业务代码经常变化,添加chunkhash避免浏览器缓存使用旧代码
// chunkhash与hash区别在于:前者是每次构建都不一样,后者是只要你的文件名是一样的,是不会变化的,一般用chunkhash多一些
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
// DefinePlugin用于在webpack构建中,定义参数,然后你可以在webpack构建配置中引用这个参数做一些配置上的判断,赋值
new webpack.DefinePlugin({
'process.env': env
}),
// js压缩插件,用于代码压缩,然后去掉注释,生成soucemap便于调试定位问题
// 构建生产环境生成sourcemap比较耗时,一般你也可以不用,在开发环境才生成sourcemap
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
sourceMap: true
}),
// extract css into its own file
// 正如上面的英文注释一样,这个插件主要是将css内容独立抽出来,而不是变成一个js模块绑如bundle中
// 官网说:这样能够加快整体构建速度,同时有利于js和css分开
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css')
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
// 用于压缩css的插件
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
// 这个是老朋友了,将我们的bundle注入到index.html,同时对html进行压缩处理,
// 这里要注意一下:
// 1.minify压缩配置
// 2.chunkSortMode:这个参数一般选择dependency,因为你可以把所有模块打包成一个文件,但是这样效率最低,一般我们会抽出
// 公共模块,产生多个bundle,引入bundle的顺序就由这插件来引入;选择 dependency,意思就是谁先被依赖,谁先被引入
// 3.inject: 有三种方式 true/'head'/'body',其实就是指指定你要把这些bundle在什么地方引入,跟你引入js文件的script标签放在哪里是一个意思
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
// split vendor js into its own file
// 这个是指定一个公共模块插件,这个插件用于定义哪些可以算是公共模块
// 构建过程中,这插件会根据我们minChunks的配置判断哪些是公共模块,抽取出来合一个name为ventor的bundle
// 我们可以看出:只要是从node_modules中出来的判定为公共模块
// 另外name为什么不是'vendor[chunkhash:7]',name不加hash值是充分利用浏览器的缓存,因为我们公共模块一般不会变化(除非技术栈升级),浏览器端有了缓存就不用重复请求
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: ['.*']
}
])
]
})
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
到这里就很清晰了:
webpack的配置思维:
var webpack = require('webpack')
var merge = require('webpack-merge')
webpack(merge(baseconfig,diffent_config)) // diffent_config指根据开发环境或生产环境做不同的配置
~~~~~~~~~~~~~~分割线~~~~~~~~~~~~~~~~~~~
现在我们来看看开发环境怎么配置,一般我们会喜欢每个以模块改动后,能够自动更新,同时不需要刷新浏览器就能看到修改。带着疑问,我们看看工程里是如何配置的。
npm run dev 对应着 node build/dev-server.js(package.json写,别忘了哈)
我们看看dev-server.js
require('./check-versions')() // 就是对应check-version.js 检查你当前 node和npm 的版本看看是否符合要求
var config = require('../config')
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
var opn = require('opn')
var path = require('path')
var express = require('express') //express,一个node的web框架
var webpack = require('webpack')
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = require('./webpack.dev.conf')
// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port // 这里其实也是使用config配置的dev.port;
// automatically open browser, if not set will be false
var autoOpenBrowser = !!config.dev.autoOpenBrowser // 服务启动成功后是否自动打开浏览器,看config里面配置了true or false
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable //
var app = express() // 新建node http server ,大家可以学一下express框架,很简单却很强大
var compiler = webpack(webpackConfig)
// webpack-dev-middleware插件是将webpack返回的compiler传给node server服务
// 这个插件的一个好处是:webpack构建的bundle都是存在内存中,而不是向硬盘输出
// 配合webpack-hot-midlleware使用,达到热更新的目的
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: true
})
// 这个就是我们的热更新了,当你改动一个模块(比如test.vue),改完按保存时,这个插件会通知compile重新对这个模块更新打包
// compile更新后,又会由devMiddleware插件将构建的内容传给node server 服务,并通知浏览器更新,达到我们不需要手动刷新浏览器就能看到我们的改动的内容
// heartbeat 心跳机制,每隔2秒检查模块是否发生变化(它怎么检查,是一件有技术的事情,通过对比chunk的id,具体怎么实现要看源码了)
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
log: false,
heartbeat: 2000
})
// force page reload when html-webpack-plugin template changes
// 编译器处理的一个编译完成的钩子函数
// 完成是调用,其实就是编译完成通知hotMiddleware 发布reload action给浏览器
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action: 'reload' })
cb()
})
})
// proxy api requests
// 在config中有个配置代理的,很多情况下,我们开发在本地,请求数据的接口在其他域名下,这个时候我们需要配置代理
// 这种配置其实个人觉得没有那么方便,因为你完完全全可以直接app.use代理一个请求,代码更加直观些
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(options.filter || context, options))
})
// handle fallback for HTML5 history API
app.use(require('connect-history-api-fallback')())
// serve webpack bundle output
app.use(devMiddleware)
// enable hot-reload and state-preserving
// compilation error display
app.use(hotMiddleware)
// serve pure static assets
// express 托管静态资源
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
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 监听编译器编译完成后执行回调函数
// 这里判断了config是否设置了自动打开浏览器
devMiddleware.waitUntilValid(() => {
console.log('> Listening at ' + uri + '\n')
// when env is testing, don't need open it
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
opn(uri)
}
_resolve()
})
var server = app.listen(port) // 启动服务
module.exports = {
ready: readyPromise,
close: () => {
server.close()
}
}
dev环境思想就是:利用devmiddle 中间件,讲webpack的编译器传给node server 服务,并搭配hotmiddle 心跳监测模块是否更改,当更改后,编译完成,由hotmiddle发布一个reload的action,然后浏览器更新显示。
而webpack的配置则是引用webpack.dev.conf.js
webpack.dev.conf.js: 同样也是引入基础配置,然后merge合并一下。有个注意点,它修改了entry,里面entry本来只是main.js,现在变成两个,build/dev-client.js 和main.js。dev-client注入个事件回调,当event.action = 'reload',是window.local.reload() 这个时候你就明白,hotmiddle发布了reload的action,浏览器为什么会更新
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf') // 同样的是引入基础配置
var HtmlWebpackPlugin = require('html-webpack-plugin')
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') // 增加友好报错插件,让我们开发中,能够更好了解报错信息
// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})
module.exports = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
},
// cheap-module-eval-source-map is faster for development
devtool: '#cheap-module-eval-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': config.dev.env
}),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
new FriendlyErrorsPlugin()
]
})
最后有个小问题:css的处理的loader到哪里去了?
其实因为vue支持less 、sass、stylus 三种预编译css语言,所以在工程里给我们封装了一个util.js,里面有个styleloader的方法,主要是根据你的vue组件里面<style>标签的lang属性,动态增加对应loader处理。大家可以看看里面是什么,挺有趣的。
系列文章:
《什么是构建? webpack打包思想?》
《webpack基础使用》
《从vue-cli学webpack配置1——针对webpack2》
《从vue-cli学webpack配置2——针对webpack3》
《webpack 、mainfest 、runtime 、缓存与CommonsChunkPlugin》
《webpack打包慢的解决方案》