前端--从Vue-cli配置学Webpack

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

2018-01-29  本文已影响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打包慢的解决方案》

上一篇下一篇

猜你喜欢

热点阅读