美文网首页前端--从Vue-cli配置学Webpack
从vue-cli学webpack配置1——针对webpack2

从vue-cli学webpack配置1——针对webpack2

作者: 下一站深圳 | 来源:发表于2018-01-29 01:15 被阅读0次

    上一篇 《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-cli的使用

    命令执行完后,在当前目录下就会看到新的文件夹,也就是你的工程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

    package.json

    --其他文件,下面讲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打包慢的解决方案》

    相关文章

      网友评论

        本文标题:从vue-cli学webpack配置1——针对webpack2

        本文链接:https://www.haomeiwen.com/subject/qwuqbxtx.html