vue-cli构建项目之webpack配置理解

概况

  • 接触了vue有一年多,基本上已经非常了解其中的用法,但是很多时候用归用,涉及到其中的配置以及一些原理知识,心虚得很。所以导致很多东西只看到表面,并不知其中,关于在vue项目当中如何理解webpack配置。
  • webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler),也就是说,webpack能够使我们的项目代码模块化,通过配置管理项目中的依赖包以及插件等,给我们提供打包压缩文件等技术。今天的项目是基于webpack 3.6.0 + vue 2.5.2 来理解其中的配置。

一、目录结构

构建项目目录结构如下,不知道怎么构建的可以网上查找一下,不再赘述。


image.png

其中

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 工具文件,主要作用分为

  1. 配置导出路径
  2. 处理各类loader相关配置
  3. 跨平台通知系统
'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>

可能,就会报错如下


image.png

这时候你就需要安装一下node-sass、sass-loader和scss

npm install node-sass
npm install sass-loader
npm install scss

安装完之后你以为万事大吉了,没想到只是从一个坑掉进另一个坑


image.png

这意思是路径错误?查找了一番之后原来是版本过高,package.json里面可以查看版本10.0.1,卸载,重新装一个低版本的7.3.1,这次真的可以了


image.png
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,页面是空白,然后报错如下


image.png

经过一番查找之后,得到结果是在config/index.js文件下dev和build下面的 assetsPublicPath '/' 修改成 './' ,改完之后发现npm run dev 的时候页面找不到了,所以为了兼容打包和运行,打包的时候统一使用config.build.assetsPublicPath,而dev下面的assetsPublicPath还是改回原来的 '/'。


image.png

image.png

打包好之后的文件在根目录下面会生成一个dist文件夹,里面的结构如下,其中,static主要存放静态资源(css、js、images、fonts等)


image.png
2. 关于devtool: 'cheap-module-eval-source-map'

为什么需要这个东西,主要的作用是帮助我们精准定位错误信息,如

image.png

我在helloWorld.vue文件调用了print文件里面的consoleLog函数,函数内容我写的很简单

let consoleLog = () =>{
    console.log('这是正确的打印');
    cosnole.error('这是错误的打印')
}

export default {
    consoleLog
}

如果没有devtool: '',那么提示的错误信息


image.png

它虽然告诉你cosnole is not defined,但是并没有告诉你在哪个文件哪一行

但是如果有devtool: 'cheap-module-eval-source-map',他的提示信息可以非常准确定位到文件以及行位置


image.png

devtool相关知识

3.关于clean-webpack-plugin 清除文件插件

按着上面插件使用方法,直接报了一个不是构造函数的错误,一脸懵


image.png
image.png

后来去看官方文档引入


image.png

使用的时候查找了很多文章有这类的使用方法

new CleanWebpackPlugin(['dist'],{
    root: path.resolve(__dirname, '..'),
    dry: false // 启用删除文件
}),

果然不行,这不是就是参数类型传得不对吗


image.png

换成下面这种参数吧,估计这个与版本有关,我的是3.0.0

new CleanWebpackPlugin({
    root: path.resolve(__dirname, '..'),
    dry: false // 启用删除文件
}),

clean-webpack-plugin相关知识

结语

很多时候,好记性真是不如烂笔头,写了一遍之后基本上知道理解其中得工作原理,以及相关一些配置,还有一些生产环境得配置已经应用还没有真正深入理解,后面有时间有精力一定补上。附上webpack官方文档,我也只看到【开发】额。。。

参考文章:
vue-cli脚手架中webpack配置基础文件详解
vue-cli的utils.js文件详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350