概况
- 接触了vue有一年多,基本上已经非常了解其中的用法,但是很多时候用归用,涉及到其中的配置以及一些原理知识,心虚得很。所以导致很多东西只看到表面,并不知其中,关于在vue项目当中如何理解webpack配置。
- webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler),也就是说,webpack能够使我们的项目代码模块化,通过配置管理项目中的依赖包以及插件等,给我们提供打包压缩文件等技术。今天的项目是基于webpack 3.6.0 + vue 2.5.2 来理解其中的配置。
一、目录结构
构建项目目录结构如下,不知道怎么构建的可以网上查找一下,不再赘述。
其中
build文件夹下面有:build.js、check-versions.js、utils.js、vue-loader.conf.js、webpack.base.conf.js、webpack.dev.conf.js、webpack.prod.conf.js
config文件夹下面有有:dev.env.js、index.js、prod.env.js
这些文件,都是关于webpack配置的文件。
二、首先理解package.json
{
"name": "openlayer", // 模块名称
"version": "1.0.0", // 版本
"description": "A Vue.js project", // 项目描述
"author": "zhufengli", // 作者
"private": true, // 私有
"scripts": { // 指定执行命令
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", // 执行npm run dev或者npm start的时候就是执行的build文件下面的webpack.dev.conf.js
"start": "npm run dev", // 启动项目命令
"build": "node build/build.js" // build命令
},
"dependencies": { // 配置依赖
"vue": "^2.5.2",
"vue-router": "^3.0.1",
"vuex": "^3.5.1"
},
"devDependencies": { // dev开发环境配置依赖
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
},
"engines": { // 指定node和npm版本
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [ // 支持浏览器配置
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
从package的执行命令配置中可以知道执行的是build文件下的webpack.dev.conf.js
三、build/webpack.dev.conf.js
'use strict'
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 { CleanWebpackPlugin } = require('clean-webpack-plugin') // 清除文件插件
const portfinder = require('portfinder') // 自动获取端口
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
// 合并了build下面的webpack.base.conf.js
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
// 资源管理配置,处理各种文件类型
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// 主要是定位错误(用于开发)
devtool: config.dev.devtool, // 'cheap-module-eval-source-map'
// 这些devServer选项应在config/index.js中自定义
devServer: {
clientLogLevel: 'warning',
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
hot: true,
contentBase: false,
compress: true,
host: HOST || config.dev.host, // ip地址
port: PORT || config.dev.port, // 端口
open: config.dev.autoOpenBrowser, // 运行npm run dev成功之后自动打开浏览器窗口
overlay: config.dev.errorOverlay
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath,
proxy: config.dev.proxyTable, // 代理
quiet: true,
watchOptions: {
poll: config.dev.poll,
}
},
// 插件配置
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env'),
'process.env.NODE_ENV': 'pro'
}),
new webpack.HotModuleReplacementPlugin(), // 热更新
new webpack.NamedModulesPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
// 这个打包本来配了两个文件,发现没效果
new HtmlWebpackPlugin({
title:"openlayer",//它的title为app,在index.html的title中间中加入<%= %>
filename: 'index.html', // 输出的文件名
template: 'index.html', // 模板文件
inject: true,
minify: {
removeComments: true, // 移除HTML中的注释
removeScriptTypeAttributes: true, // 删除script的类型属性,在h5下面script的type默认值:text/javascript 默认值false
removeAttributeQuotes: true, // 是否移除属性的引号 默认false
useShortDoctype: true, // 使用短的文档类型,默认false
decodeEntities: true,
collapseWhitespace: true, // 删除空白符与换行符
minifyCSS: true // 是否压缩html里的css(使用clean-css进行的压缩) 默认值false
},
hash:true,
chunks:['app']
}),
new HtmlWebpackPlugin({
title:"test",
filename: 'test.html',
template: 'test.html',
hash:true,
inject:true,
chunks:['test']
}),
new CleanWebpackPlugin({
root: path.resolve(__dirname, '..'),
dry: false // 启用删除文件
}),
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 {
process.env.PORT = port
// 将端口添加到devServer配置
devWebpackConfig.devServer.port = port
// 添加 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)
}
})
})
通过webpack.dev.conf.js merge合并文件 webpack.base.conf.js 可以知道基础的配置文件都在这里
四、build/webpack.base.conf.js
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
context: path.resolve(__dirname, '../'),
// 入口文件配置
entry: {
app: './src/main.js', // 入口文件
test: './src/test.js'
},
// 出口文件配置
output: {
path: config.build.assetsRoot, // 输出文件路径
filename: '[name].js', // 输出文件名
publicPath: process.env.NODE_ENV === 'pro'
? config.build.assetsPublicPath
: config.build.assetsPublicPath // 公共存放路径
// 为什么用一样的路劲config.build.assetsPublicPath,下面详解
},
resolve: {
// 扩展文件后缀,这样在引入文件的时候可以忽略后缀名
// 如 import 'js/index' js/index.js
extensions: ['.js', '.vue', '.json'],
// 配置别名
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
'src': resolve('src'),
}
},
// 文件处理
module: {
rules: [
// vue文件语法处理
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig // (下面详解)
},
// 语法处理,会处理成浏览器能够识别的ES5语法
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
// 图片处理
{
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]')
}
}
]
},
node: {
//防止Webpack注入无用的setImmediate polyfill。
setImmediate: false,
// 阻止webpack将模拟注入到Node本机模块
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}
五、build/webpack.prod.conf.js
生产环境配置,暂不深入理解,想要了解的小伙伴可以看下这篇文章 webpack.prod.conf.js文件详解
六、build/vue-loader.conf.js
这个文件主要是处理vue文件,主要是sass、less用的比较多,这里需要更加正确理解。
'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction ? config.build.productionSourceMap : config.dev.cssSourceMap
module.exports = {
// css规则处理,包括sass、less、postcss等
loaders: utils.cssLoaders({
sourceMap: sourceMapEnabled, // 调式作用
extract: isProduction
}),
cssSourceMap: sourceMapEnabled,
cacheBusting: config.dev.cacheBusting,
// 可以将某些属性转成require调用
transformToRequire: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
}
七、build/utils.js
utils 工具文件,主要作用分为
- 配置导出路径
- 处理各类loader相关配置
- 跨平台通知系统
'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin') // 用来将文本从bundle中提取到一个单独的文件中
const packageConfig = require('../package.json')
// 导出路径
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory : config.dev.assetsSubDirectory
// 返回一个完整路径的相对根路径
return path.posix.join(assetsSubDirectory, _path)
}
// 导出cssLoaders相关配置
exports.cssLoaders = function (options) {
options = options || {}
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
// 生成用于提取文本插件的加载程序字符串
function generateLoaders (loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// 指定该选项时提取CSS(生产构建)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(), // 对应 vue-style-loader 和 css-loader
postcss: generateLoaders(), // 对应 vue-style-loader 和 css-loader
less: generateLoaders('less'), // 对应 vue-style-loader 和 less-loader
sass: generateLoaders('sass', { indentedSyntax: true }), // 对应 vue-style-loader 和 sass-loader
scss: generateLoaders('sass'), // 对应 vue-style-loader 和 sass-loader
stylus: generateLoaders('stylus'), // 对应 vue-style-loader 和 stylus-loader
styl: generateLoaders('stylus') // 对应 vue-style-loader 和 styl-loader
}
}
// 为独立样式文件生成加载程序(.vue之外)
exports.styleLoaders = function (options) {
const output = []
// 生成的各种css文件的loader对象
const loaders = exports.cssLoaders(options)
// 把每个文件的loader提取出来,push到output数组中
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
// 发送跨平台通知系统
exports.createNotifierCallback = () => {
const notifier = require('node-notifier')
// 出现error时触发
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: packageConfig.name, // 标题
message: severity + ': ' + error.name, // 内容
subtitle: filename || '', // 短标题
icon: path.join(__dirname, 'logo.png') // 图标
})
}
}
关于sass和less,在我们初始化项目的时候并没有默认安装,而在项目中,基本上都有用到,比如写一个.sass文件或者写一段代码
<style lang="sass" scoped>
</style>
可能,就会报错如下
这时候你就需要安装一下node-sass、sass-loader和scss
npm install node-sass
npm install sass-loader
npm install scss
安装完之后你以为万事大吉了,没想到只是从一个坑掉进另一个坑
这意思是路径错误?查找了一番之后原来是版本过高,package.json里面可以查看版本10.0.1,卸载,重新装一个低版本的7.3.1,这次真的可以了
npm uninstall sass-loader
npm install sass-loader@7.3.1
七、build/build.js和build/check-versions.js
暂不深入理解,想要了解的小伙伴可以看下参考文章 vue-cli脚手架中webpack配置基础文件详解
讲完build文件夹,还有一个config文件夹,这个文件夹主是定义一些变量exports出去给build文件夹下面的文件使用
八、config/dev.env.js
开发环境配置
'use strict'
module.exports = {
NODE_ENV: '"dev"'
}
九、config/prod.env.js
生产环境配置
'use strict'
module.exports = {
NODE_ENV: '"pro"'
}
八、config/index.js
定义一些开发/打包需要的变量
'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')
module.exports = {
dev: {
assetsSubDirectory: 'static',
assetsPublicPath: '/', // 公共路劲
proxyTable: {},
// 各种开发服务器设置
host: 'localhost', // 可以被process.env.HOST覆盖
port: 8080, //端口, 可以被process.env.PORT覆盖,如果正在使用端口,则将确定一个空闲端口
autoOpenBrowser: false, // 运行自动打开浏览器
errorOverlay: true, // 错误提示
notifyOnErrors: true, // 跨平台错误提示
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
/**
* Source Maps 代码调试BUG、错误等
https://webpack.js.org/configuration/devtool/#development
*/
devtool: 'cheap-module-eval-source-map',
// cheap-module-eval-source-map 开发环境(dev)推荐使用
// cheap-module-source-map 可以定位生产环境的代码报错
//如果在devtools中调试vue文件时遇到问题,
//将其设置为false-可能会有所帮助
// https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true, // 缓存失效
cssSourceMap: true
},
// 打包配置
build: {
index: path.resolve(__dirname, '../dist/index.html'), // 编译后生成的文件位置
assetsRoot: path.resolve(__dirname, '../dist'), // 打包后存放代码位置
assetsSubDirectory: 'static', // 静态文件夹(js、css、images)
assetsPublicPath: './', // 发布根目录
productionSourceMap: true,
devtool: '#source-map',
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
bundleAnalyzerReport: process.env.npm_config_report
}
}
十一、项目遇坑记
为了弄清楚一下项目中使用的插件,以及一些重要的属性,进行的一些属性试验
1. 关于npm run build 打包编译
这里有个问题是关于输出公共存放路径 config.build.assetsPublicPath 在我打包完之后打开dist/index.html,页面是空白,然后报错如下
经过一番查找之后,得到结果是在config/index.js文件下dev和build下面的 assetsPublicPath '/' 修改成 './' ,改完之后发现npm run dev 的时候页面找不到了,所以为了兼容打包和运行,打包的时候统一使用config.build.assetsPublicPath,而dev下面的assetsPublicPath还是改回原来的 '/'。
打包好之后的文件在根目录下面会生成一个dist文件夹,里面的结构如下,其中,static主要存放静态资源(css、js、images、fonts等)
2. 关于devtool: 'cheap-module-eval-source-map'
为什么需要这个东西,主要的作用是帮助我们精准定位错误信息,如
我在helloWorld.vue文件调用了print文件里面的consoleLog函数,函数内容我写的很简单
let consoleLog = () =>{
console.log('这是正确的打印');
cosnole.error('这是错误的打印')
}
export default {
consoleLog
}
如果没有devtool: '',那么提示的错误信息
它虽然告诉你cosnole is not defined,但是并没有告诉你在哪个文件哪一行
但是如果有devtool: 'cheap-module-eval-source-map',他的提示信息可以非常准确定位到文件以及行位置
3.关于clean-webpack-plugin 清除文件插件
按着上面插件使用方法,直接报了一个不是构造函数的错误,一脸懵
后来去看官方文档引入
使用的时候查找了很多文章有这类的使用方法
new CleanWebpackPlugin(['dist'],{
root: path.resolve(__dirname, '..'),
dry: false // 启用删除文件
}),
果然不行,这不是就是参数类型传得不对吗
换成下面这种参数吧,估计这个与版本有关,我的是3.0.0
new CleanWebpackPlugin({
root: path.resolve(__dirname, '..'),
dry: false // 启用删除文件
}),
结语
很多时候,好记性真是不如烂笔头,写了一遍之后基本上知道理解其中得工作原理,以及相关一些配置,还有一些生产环境得配置已经应用还没有真正深入理解,后面有时间有精力一定补上。附上webpack官方文档,我也只看到【开发】额。。。