基于vue2.x的webpack升级/项目搭建指南

789 阅读18分钟
first thing:绝不标题党、面试没帮助、新手不友好
有人看最好不过的背景:

  十月初对公司产品的前端构建做了一些优化,但还遗留了一些问题:

  1.热重载过慢:单文件改动,热重载的十次平均响应时间约为17s,严重影响开发体验;

  2.某些bundle体积过大,导致单个资源请求耗时过多,浏览器加载速度收到影响;

  3.没有liint机制去控制编码过程中的语法规范,也没做代码保存的自动格式化,代码质量低,组员编码风格迥异、交接成本高。

  4.打包体积与打包速度仍有优化空间。

  综合考虑,个人判断webpack1的性能不足以为前端项目的构建流程提供更好的支持,升级webpack,改善构建体验,造福运维与测试同事hhhh

这次会有更多图的正文:

       本文将演示webpackv1.13.2升级到v4.44.2的过程,选择这个版本的原因比较主观,我认为它是webpack4的最新一个小版本(2020.11),webpack4从发布测试版本到现在为止已经有两年多了,两年里的迭代和bug修复,足够让这个大版本的功能变得完善和稳定到让人信任。这次升级过程中我将删除原来的配置文件,从零开始进行升级,因此本文兴许也可以当作一个用webpack构建项目的入门教程。

  package.json里有个devDependencies,记录了项目在开发环境下需要的依赖,在做好文件备份后,我将node_modules删除,将devDependencies的列表清空;然后npm i。

  然后我开始实现一个最简化的版本,我装上了4.x版本最新的webpack:

npm i webpack@4.44.2 -D

   webpack4.x版本需要命令行工具才能运行,所以我们还需要去下载webpack-cli,我就随便选了一个不算新也不算旧的版本:

npm i webpack-cli@3.3.9 -D

  然后开始写配置文件,首先写一个基本版的测试一下新版本webpack的可行性:

  先创建一个简单的入口文件test.js供打包用:

1 import {cloneDeep} from "lodash"
2 const obj = {color:'red'}
3 const copy = cloneDeep(obj)

  在项目根目录下创建webpack.config.js文件:

const path = require('path');
module.exports = {
    entry: "./src/test.js",
    output: {
        path: path.resolve(__dirname,"dist"),
        filename: 'testbundle.js',
    }
}

  webpack启动时,如果未指定运行的文件,就会自动读取根目录下的webpack.config.js中的配置,现在修改package.json的scripts中的build命令:

  "scripts": {
    "build": "webpack"
  },

  运行npm run build,webpack便会按配置进行打包,然后你会看到你的dist目录中多出一个名为testbundle.js的文件。

  同时,按照我这个配置,会在控制台到看到一个警告:

WARNING in configurationThe 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.You can also set it to 'none' to disable any default behavior. Learn more: webpack.js.org/configurati…

  这个警告表示当前传给webpack的配置中没有设置“mode”属性,对此webpack将视作mode: "production"来处理,这里需要提到webpack4配置文件中的mode属性,webpack4内置了一些比较通用的插件配置,省去了开发者为配置webpack而消耗的时间精力,使用mode属性就可以快捷配置这两套插件,mode:"development"跟mode:"production"分别就对开发环境与生产环境两种场景做了优化,比如持久化缓存、代码压缩等,如果你不想使用这两种预设的任意一种,可以将mode的值设为"none"。至于其他细节,感兴趣的朋友可以从文档获取更多信息

  现在,我们把配置文件改一改,常规的思路是将开发环境的配置跟产品环境的配置分离成两个文件,一般命名为webpack.dev.conf.js和webpack.prod.conf.js,因为这两个场景下的配置都有部分共同之处,所以又可以抽出一个公共的配置文件webpack.base.conf.js,目前我们先不去考虑生产环境与开发环境下的差异,先创建一个基本配置webpack.base.js,让webpack能够正确地解析一个vue文件,顺利完成打包。

  首先解析.vue文件,需要安装vue-loader以及与vue同版本号的vue-template-loader,这里需要注意的是vue-loader版本如果在15及以上,需要额外从vue-loader的目录里引入VueLoaderPlugin,VueLoaderPlugin将使用你在rules中定义的其他规则来检查和处理.vue文件中符合规则的语句块

const VueLoaderPlugin = require("vue-loader/lib/plugin");
...
...
...
module: {  
    rules: [    
        {      
            test: "/\.vue$/",
            loader: "vue-loader"
    }  
    ]
},

plugins: [
  new VueLoaderPlugin()
]

  如果这时候你已经看到本文的更下面并且写好了build文件,或者是在webpack.config.js的基础之上改写配置文件,此时执行打包命令你将会发现控制台输出了很多错误,例如:

  满屏的红字有些吓人,但仔细看看就会发现其实并不是什么大不了的问题,截图上有一段样式代码,并且报错提示你可能需要其他loader去处理vue-loader的解析结果,所以为了解决截图上的问题能,使webpack能够顺利对样式代码进行处理,你需要添加相应的loader,添加什么由你的项目具体使用情况决定:

npm i css-loader style-loader url-loader file-loader less less-loader sass node-sass stylus stylus-loader -D

  css-loader用于解析css代码,style-loader则生成style标签将css挂载在到页面结构中,file-loader读取静态资源的引用路径,在输出目录中生成符合规则的文件,供编译后的代码使用,url-loader在file-loader的基础之上,将体积小于指定数值的文件转码成base64字符串,可通过这种方式减少资源请求数。其他文件其他loader以及相关依赖不再赘述。  

  css相关loader的载入我沿用了项目之前的写法(反正也是从别的地方抄来的),稍微加了些改动:

exports.cssLoaders = function () {
     // style-loader改为使用vue-style-loader,除了具备与style-loader一样的功能之外,还实现了不需要页面刷新的样式层面的热重载(来自vue-laoder官网描述)
      // 但style-loader在很久之前就已经补全了这个功能,所以你没有在用SSR,也可以选择使用最新版的style-loader配套css-loader,这两个loader现在esmodule属性都默认为true。

  const vueStyleLoader = {
    loader: "vue-style-loader"
  }  
  const cssLoader = {
    loader: "css-loader",   // 如果你使用的是vue-style-loader并且css-loader的版本在v4.0.0及以上,这个属性必须加上。
    options: {     
        esModule: false   
    }
  }
  // 当在一条规则中应用多个loader时,loader的执行顺序从右至左,所以预处理语言相关的loader摆右边  // 如果generateLoaders没有接收到参数,将以返回基础的loader配置:使用css-loader与vue-style-loader
  function generateLoaders (loader) {
    const outputLoaders = [vueStyleLoader,cssLoader]
    if (loader) {
      const targetloader = {loader:loader+"-loader"}
      outputLoaders.push(targetloader)
    }
    return outputLoaders
  }  
  return {
    css: generateLoaders(),
    less: generateLoaders("less"),
    sass: generateLoaders("sass"),
    scss: generateLoaders("sass"),
    stylus: generateLoaders("stylus"),
    styl: generateLoaders("stylus")
  }
}
exports.styleLoaders = function () {
  var output = []
  var loaders = exports.cssLoaders()
  
  for (let extension in loaders) {
    var loader = loaders[extension]
    console.log(loader)
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }
  return output
}

  这个丐版的styleLoaders输出了一个保存了处理样式文件规则的数组,你可以使用拓展运算符将这些规则挂载到rules中。所以接下来我们创建一个build.js,用这个文件调起webpack的api进行打包。我比较倾向于这种写法,用命令行调用node执行一个build文件,在这个文件中运行webpack,这样写在处理不同打包配置的场景时要稍微方便一些,比如有的公司就分sit、uat、prod(生产)等好几套环境,会对应不同的全局配置(如接口的的baseurl、请求加解密、局部打包等等),这种情况下可以通过process.argv来获取命令行参数,细化配置;对于我来说另外一个好处是方便加old_space参数给内存扩容,避免一些稍大的项目运行过程中出现内存不够导致编译失败的问题(64位windows给node分配的内存大概是1.4G)

丐版build.js

process.env.NODE_ENV = 'production'

var webpack = require('webpack')
var webpackConfig = require('./webpack.prod.conf')


webpack(webpackConfig, function (err, stats) {
  // spinner.stop()
  if (err) throw err
  process.stdout.write(stats.toString({
    colors: true,
    modules: false,
    children: false,
    chunks: false,
    chunkModules: false
  }) + '\n')
})

  然后把build命令改写为:

"build": "node --max_old_space_size=2077 build/build.js"

  现在执行npm run build,看看会发生什么:

  过程没报错,一个基础的打包流程,到现在其实就走完了,这个包实际上也不能用,但我认为我认为这还是有意义的一步,接下来以这个版本继续进行功能丰富。接下来的内容不具有连续性,没什么前置要求,你可以选择感兴趣的看:

  1. clean-webpack-plugin

  2. html-webpack-plugin

  3. babel & babel-loader

  4. postcss

  5. webpack-dev-server

  6. mini-css-extract-plugin

  7. terser-webbpck-plugin & optimize-css-assets-webpack-plugin

  8. image-webpack-loader

  9. thread-loader

  10. cache-loader

  11. eslint & eslint-loader

正式开始之前:

  之前介绍过了,webpack4的mode属性有"development"和"production"两种,对应开发环境与生产环境,由于两个环境下的配置会通常有差异,使用单一的配置其实不太能适应接下来的应用场景,所以接下来在追加配置之前,你也许需要分化出对应的配置文件,在不同的场景下加载不同的打包配置,这也是webpack的常规操作了。

  对一般项目来说,在webpack.base.conf的基础之上,新增两个配置文件就够用了,按照我这边的惯例,命名为webpack.dev.conf.js和webpack.prod.conf.js,

  然后我要介绍webpack-merge,webpack-merge一般用来合并两个配置文件,用法如下:

const merge = require("webpack-merge").merge
const webapckBaseConfig = require("./webpack.base.conf")
module.exports = merge(webapckBaseConfig,{
    ...your configuration
})

  ps:当然你也可以把它当成Object.assign方法,用来合并一个普通的对象,就是有点大炮打蚊子的味道(webpack-merge主要对loader中的rules,尤其是各个具体的loader的option做了追加、替换处理)

html-webpack-plugin

  在上一段内容的最后得到的dist目录的结构描述成json是这样的:

"dist":[

  "static": [

    {”img": [...]},

    {"font": [...]},

    {"js":[...]}

  ]
]

  这种形式的包还没有办法应用到实践中,原因是缺少一个用于挂载js脚本的和css文件的“实体”,如果没有一个html文件作为入口用来挂载这些资源,浏览器就没有机会去解析style/script标签和关联的资源/代码,所以页面自然是打不开的。解决这个问题需要创建一个html文件来引入打包资源,而html-webpack-plugin这个插件帮助我们简化了这个流程(我的意思是,虽然不推荐,但你确实可以自己在一个html文件中手动把static目录中的资源引入来使你的前端包变得可用)。

  接下来我在webpack的配置中加上这个插件,下面会挑几个属性解释它们的作用,其余属性请看

const HtmlWebpackPlugin = require("html-webpack-plugin")
...
...
plugins: [
    ...
    new HtmlWebpackPlugin({
     // 选择一个本地的html文件作为模板而不是让插件自动生成
      template:"xxx/xxx/index.html",
      // inject这个属性可以选择在html文件中引入打包资源的位置,true是默认值,将script注入到body标签的最下方,其余可选值为:"head"/"body"/false
      inject: true,
      // 生成的html文件名,"index.html"是默认值
      filename:"index.html",
     // 小图标
      favicon:"xxxx.png",
     // 破坏缓存,在引入每个资源时以?xxxxxx的形式添加哈希作为请求参数,效果是否与output中的filename中添加[hash]一致这点就有待验证
    // 因为其他执行过程中会创建新文件的插件比如copy-webpack-plugin和mini-css-extract-plugin都是可以选择是在生成的文件名后添加hash后缀的
      // hash: "true"
    })
   // ps:这个插件也是免配置的,如果没什么特殊配置的话,可以直接写new HtmlWebpackPlugin()
]

  接下来执行打包命令,打包后dist目录就多出了一个index.html:

  细节:

  到这里关于这个插件的介绍就结束了,虽然几乎每个使用webpack的项目都会用这个插件,但实际上我觉得他的重要性并不是那么高,可能是因为我这边的需求还用不到其他进阶的属性吧。

babel-loader & babel

   babel用于将es6以及更前沿的语法转成es5语法以便于代码能在低版本环境中运行,使用babel转译到低级语法算是绝大多数项目在构建过程中普遍存在的流程,如果你的项目是从零开始构建而不是在老版本的基础上重新改的话,你在配置过程中的时候也许会比后一种方式少花很多时间。

  话不多说,上代码。

  项目之前使用的是babel-core@6.0.0以及babel-loader@6.0.0,我把该备份的备份好后,然后开始重新配置。

  首先需要下载基本的依赖

npm install --save-dev @babel/core @babel/cli @babel/preset-env
npm install --save @babel/polyfill
npm install babel-loader -D

  babel7.x要求搭配v8.x的babel-loader,写这篇文章的时候最新的babel-loader版本是8.2.2,所以我就直接安装了,安装完成之后开始对webpack追加配置,使用babel-loader对js文件进行处理:

rules: [
      ...    
      {
        test: /\.js$/,
        loader: "babel-loader",
        // 忽略node_modules目录
        exclude: /node_modules/,
      },
]

  babel的配置文件命名分两种类型,一种规定命名为babel.config.xx(拓展名一般是json或者js,你也可以视你的环境和写法设为cjs和mjs),另一种为.babelrc(json)或者.babelrc.js,其中babelrc的优先级比babel.config更高。在项目的根目录下创建这个文件,babel-loader便会根据这个配置来处理符合规则的js文件。

{
    // 通过presets配置babel的预设,你可以理解为一系列插件的集合,在预设后可以添加一些这些插件的共同配置
    // 而preset-env又可以说是几个预设的集合,默认情况下会加载es5到目前最新的preset(es2020),并且内容随着新标准的推出而更新
    // 但babel这样全面的支持带来了不小的的负面效果,比如transform后的代码体积越来越大(确实)、编译速度也会受到影响(确实),所以为了回避这个问题,官方并不推荐用户0配置直接使用babel,用户提供的配置的粒度越精细,babel的性能表现就会越好(这是我根据官网描述理解的说法,但实际上并没有感觉出来有啥差别,嘻嘻)
    "presets": [["@babel/preset-env",{
        "targets": {
            "browsers": [
              "> 1%",
              "last 2 versions",
              "not ie <= 10"
            ]
        }
    }
    ]],
    "plugins": [
        // 转换vue单文件组件script模块中可能会出现的jsx语法
        "transform-vue-jsx", 
        // 项目使用到的elementui按需导入
        [
            "component",
            {
                "libraryName": "element-ui",
                "styleLibraryName": "theme-chalk"
            }
        ]
    ],
    "comments": false
}

postcss

  postcss是一个用于解析与转换css代码的平台类型的工具,我之所以称之为平台,是因为它通过搭载在平台上的应用了实现对css代码的各种处理,比如lint机制和客户端兼容等,这里只展示我这边构建过程中使用到的客户端兼容工具autoprefixer的用法,之后可能会用demo水一篇的过程细节出来。

  在webpack中运用postcss-autoprefixer需要配合postcss-loader,所以老样子,第一步需要安装依赖;

  npm i postcss postcss-loader autoprefixer -D

  在构建过程中,postcss-loader会尝试读取根目录下名为postcss.config.js的postcss配置,所以接下来需要新建一个postcss.config.js文件:

module.exports = {
    plugins: {
      "autoprefixer": {
      }
    }
}

  autoprefixer需要声明支持的客户端名单,如果你不想使用默认配置,可以在根目录下建一个.browserslistrc文件来配置需要兼容的客户端的版本范围,也可以在package.json中添加一个browsersList属性,比如:

...
  "browserslist": [
  // 全球范围内超过1%人使用的浏览器…即是说这个数字量越大兼容的客户端范围就越小
    "> 1%",
  // 浏览器厂商最近发布的n个版本
    "last 2 versions",
  // 不兼容ie10及以下
    "not ie <= 10"
  ]
...

  有时候你并不确定你是否需要使用postcss的功能,这种情况下设置一个开关是不错的选择,添加postcss-loader处理css代码的流程需要在css-loader处理之前,因为处理样式的loader之前被封装到了名为utils的文件里,结合新增的开关,你需要拓展utils.js中的styleLoaders方法:

/**
 * 
 * @param {{usePostcss:Boolean}} options 
 */
exports.cssLoaders = function (options) {

  const vueStyleLoader = {
    loader: "vue-style-loader"
  }
  const cssLoader = {
    loader: "css-loader",
    options: {
      // 如果你使用的是vue-style-loader并且css-loader的版本在v4.0.0及以上,下面这个属性必须配置为false,具体原因请看https://www.cnblogs.com/byur/p/14194672.html
      esModule: false,
    }
  }
  const postcssLoader = {loader:'postcss-loader'}
  // loader解析顺序从右至左
  // const baseLoaders = [vueStyleLoader,cssLoader]
  function generateLoaders (loader) {
    const outputLoaders = [vueStyleLoader,cssLoader]
    if (options.usePostcss){
      outputLoaders.push(postcssLoader)
    }
    if (loader) {
      const targetloader = {loader:loader+"-loader"}
      outputLoaders.push(targetloader)
    }
    return outputLoaders
  }

  return {
    css: generateLoaders(),
    less: generateLoaders("less"),
    sass: generateLoaders("sass"),
    scss: generateLoaders("sass"),
    stylus: generateLoaders("stylus"),
    styl: generateLoaders("stylus")
  }
}
// 追加了参数options
exports.styleLoaders = function (options) {
  var output = []
  // 透传options
  var loaders = exports.cssLoaders(options)
  
  for (let extension in loaders) {
    var loader = loaders[extension]
    console.log(loader)
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }
  return output
}

  在必要的时候开启它:

const merge = require("webpack-merge").merge
const webapckBaseConfig = require("./webpack.base.conf")
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const config = require("../config")
const utils = require('./utils')
module.exports = merge(webapckBaseConfig,{
    mode:"production",
    module: {
        // 需要在config.build中将usePostcss设置为true
        rules: utils.styleLoaders({
            usePostcss:config.build.usePostcss
        }),
    },
    plugins: [
        new CleanWebpackPlugin(),
    ]
})

   大功告成。

  postscss这部分内容其实是有一些特殊的,特殊在没什么存在感(笑),一是因为很多项目实际上只用上了一个autoprefixer,但这个东西实际上想要达到准确理解配置的意义之后再去根据自己的需求进行调整,还是有些麻烦的,所以很多时候就直接复制一份配置到新项目就完事了;而第二点在于autoprefixer这个工具,以开发者(仅代表我自己)的角度,很多时候不太容易察觉到autoprefixer生效与不生效时的区别,你只知道你做的配置当然是在构建过程中生效了,但是不生效的时候客户端的表现是个什么样子,这个我平时是很少遇到的,至少在使用主流属性(比如placeholder、box-shadow等)的时候,所以有时候心里不免怀疑它的必要性。

webpack-dev-server

  接下来增加一个开发模式,主要是用来落实“开发体验”这四个字,webpack的开发模式按官网的说法分为三种(watch mode、webpack-dev-server、webpack-dev-middleware),但实际应用中基本只会使用后两种(因为第一种不能自动刷新浏览器),公司项目原先使用的是中间件,我这次将改用webpack-dev-server来作为开发模式下的平台。

  所以之前新建的webpack.dev.conf.js现在发挥了用武之地,这个文件除了继承了通用配置之外,还将用来承载开发模式下的一些个性化配置,这其中有一个重要的属性便是devServer,在webpack默认寻找并且执行的配置文件webpack.config.js中,devServer也是需要在其中配置的,包括在webpack基础之上封装的vue-cli,你也可以在它的配置文件中找到devServer的属性,现在我们把这个属性配置webpack.dev.conf.js文件中。

webpack.dev.conf.js

const merge = require("webpack-merge").merge
const Webpack = require("webpack")
const webapckBaseConfig = require("./webpack.base.conf")
const config = require("../config")
const utils = require('./utils')
const path = require("path")


module.exports = merge(webapckBaseConfig,{
    mode:"development",
    module: {
        rules: utils.styleLoaders({
            usePostcss:config.dev.usePostcss
        }),
    },
    plugins: [
        new Webpack.HotModuleReplacementPlugin(),
    ],
    devtool:config.dev.devtool,
    devServer: {
        // 在控制台展示构建进度
        progress: true,
        // 内联模式,发生热替换时,相关的构建信息将刷新在控制台中,false则展示在浏览器中,建议用trueinline: true,
        // 日志级别
        clientLogLevel: "warning",
        // 可以理解为静默模式,webpack编译过程中的错误和警告将不会输出在控制台,构建/热重载完成后不会有提示,如果没有其他辅助输出的工具,不建议设置为false
        quiet: false, 

        historyApiFallback: {
            rewrites: [
                {
                  from: /.*/,
                  to: path.posix.join(config.dev.assetsPublicPath, "index.html"),
                },
            ],
        },
        // 开启了hot之后,如果插件里没有添加HotModuleReplacementPlugin(HMR)的话,构建开始时dev-server会自动帮你补上,但还是建议手动添加吧hhh
        hot: true,
        // 开启gzip
        compress: true,
        host: config.dev.host,
        port: config.dev.port,
        // 自动打开浏览器
        open: config.dev.autoOpenBrowser,
        // 编译失败时在浏览器全屏展示报错
        overlay: config.dev.errorOverlay?
            {
                warnings: false,
                errors: true,
            }:false,
        publicPath: config.dev.assetsPublicPath,
        // 代理
        proxy: config.dev.proxyTable,
    },
})

   按照常规的写法npm命令行写webpack-dev-server build/webpack.dev.conf.js就够了,但是之前提到过了,这样有时候分配的内存会不够用,我摸索出来的的解决办法使创建一个js脚本,通过node携带扩容参数去执行这个脚本并且调动webpack-dev-server的api。

新建一个dev-server.js
/**
 * 写法参考:
 * https://github.com/webpack/webpack-dev-server/blob/master/examples/api/simple/server.js  node调起dev-server
 */
const Webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const webpackConfig = require('./webpack.dev.conf');

const compiler = Webpack(webpackConfig);
const devServerOptions = webpackConfig.devServer
// 不同于手动调webpack启动项目,手动调起WebpackDevServer的时候,会忽略webpackConfig中的devServer,所以需要在第二个参数中补充
const server = new WebpackDevServer(compiler, devServerOptions);

server.listen(devServerOptions.port, devServerOptions.host, () => {
  console.log(`Starting server on http://${devServerOptions.host}:${devServerOptions.port}`);
});



// package.json
……
  "scripts": {
    "build": "node --max_old_space_size=4096 build/build.js",
    "dev": "node --max_old_space_size=4096 build/dev-server.js"
  },
……

  到这里你已经写出了一个基础功能还算完备的配置(用来自娱自乐应该足够了),可以打包发到服务器上部署,也可以本地跑起来开发调试,接下来需要做的是兼顾体验与性能、对现有配置进行优化,希望我的内容和文笔不会让你感到乏味。

  现在,一切用数据说话

接下来用webpack-bundle-analyzer、speed-measure-webpack-plugin两个插件来对现有项目构建流程中的性能表现进行分析。而有一个需要事先说明的事情是,这两个插件在生效期间也是消耗了部分性能的,所以耗时的计算和webpack计算出来的总耗时会有误差,一般来说webpack的统计信息中的耗时会比speed-measure-webpack-plugin会少,因为webpack-bundle-analyzer的耗时会被speed-measure-webpack-plugin统计。

  我跑了五六次,掐头去尾去了一次最贴近平均值的结果:

   bundle的整体视图:

   附带的一些统计信息:

  (看到这里stat和parsed的数值对比,可能就有懂哥意识到了这个构建过程中可能重复打包了依赖)

耗时细节的统计信息:

  这种几乎零个性化配置的性能比这篇文章中最后的配置还要好点,让我感叹webpack4对相比之前的版本,不光在性能上有所增强,对用户也友好了太多……(早该管管了!.jpg)

  css抽取与代码压缩

  为了从loader和plugin上挤出更多时间,接下来我在mode:production的基础之上,进行样式代码提取以及代码压缩。

  webpack4之前我这边做css提取使用的是extract-text-webpack-plugin,webpack4之后extract-text-webpack-plugin不再适用了,官方建议使用mini-css-extract-plugin替代。

  mini-css-extract-plugin需要配合它内置的loader一起使用,所以你需要在之前写好的utils.js中进行相应的配置,由于development模式下你其实并不需要进行代码提取/压缩等操作,所以写的时候需要增加判断场景的逻辑:

(为什么不能像博客园一样折叠代码啊?!!)

// utils.js
const path = require('path')
const config = require('../config')
// 引入mini-css-extract-plugin内置的loader
const MiniCssExtractPluginLoader = require("mini-css-extract-plugin").loader
exports.assetsPath = function (_path) {
  var assetsSubDirectory = process.env.NODE_ENV === 'production'
    ? config.build.assetsSubDirectory
    : config.dev.assetsSubDirectory;
  return path.posix.join(assetsSubDirectory, _path)
}
/**
 * 
 * @param {{usePostcss:Boolean}} options 
 */
exports.cssLoaders = function (options) {

  const vueStyleLoader = {
    loader: "vue-style-loader"
  }
  const cssLoader = {
    loader: "css-loader",
    options: {
      // 如果你使用的是vue-style-loader并且css-loader的版本在v4.0.0及以上,下面这个属性必须配置为false,具体原因请看https://www.cnblogs.com/byur/p/14194672.html
      esModule: false,
    }
  }
  const postcssLoader = {loader:'postcss-loader'}
  // loader解析顺序从右至左
  // const baseLoaders = [vueStyleLoader,cssLoader]
  function generateLoaders (loader) {
    const outputLoaders = [vueStyleLoader,cssLoader]
    // 传入配置中extractCss为true时,插入MiniCssExtractPluginLoader
    if (options.extractCss){
      outputLoaders.splice(1,0,{
        loader:MiniCssExtractPluginLoader,
        options: {
          publicPath: "../../",
        },
      })
    }
    if (options.usePostcss){
      outputLoaders.push(postcssLoader)
    }
    if (loader) {
      const targetloader = {loader:loader+"-loader"}
      outputLoaders.push(targetloader)
    }
    return outputLoaders
  }

  return {
    css: generateLoaders(),
    less: generateLoaders("less"),
    sass: generateLoaders("sass"),
    scss: generateLoaders("sass"),
    stylus: generateLoaders("stylus"),
    styl: generateLoaders("stylus")
  }
}

exports.styleLoaders = function (options) {
  var output = []
  // 透传options
  var loaders = exports.cssLoaders(options)
  
  for (let extension in loaders) {
    var loader = loaders[extension]
    console.log(loader)
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }
  return output
}

// webpack.prod.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
……
    plugins: [
        new MiniCssExtractPlugin({
            filename: "static/styles/[name][contenthash:7].css"
       ignoreOrder: true,
        })
    ]
……

  至此,在你构建的时候,css代码会被抽取并以css文件的形式存储在dist/static/styles路径中,接下来对css代码顺便加上js代码进行压缩,这个过程主要通过配置optimization属性来完成。对于其中的一些配置,我在注释里写了些个人的理解,有不对的地方的地方请多指正。

// 同样是webpack.prod.js的另一部分
const OptimizeCSSPlugin = require("optimize-css-assets-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin")

    optimization: {
        flagIncludedChunks:true,
        occurrenceOrder:true,
        concatenateModules:true,
        usedExports: true,
        // production模式下以上属性默认开启

        // mode:production下minimize属性默认为true,为true时,若minimizer未配置,则使用terser-webpack-plugin对js代码进行压缩优化。
        minimize: true,
        // 在mode:production下js的代码压缩是自动开启的,这里我根据自己的需要,增加了一些额外的配置。为此你需要指定minimizer来分别配置处理css和js代码的插件。
        minimizer:[
            // 跟官网文档示例一样,我使用terser-webpack-plugin作为js代码压缩的工具,当然也可以使用第三方的其他插件;需要注意的是terser-webpack-plugin,现在最新版本是v5.x,对应的webpack版本是v5.x,         在webpack4上使用是会报错的。
            new TerserPlugin({
                cache: true,
                parallel: true,
                sourceMap: true,
                // 不单独提取注释
                extractComments: false,
                terserOptions: {
                    sourceMap: true,
                    // 从语义上便可理解
                    compress: {
                        drop_console: true,
                        drop_debugger: true,
                        // pure_funcs接收一个list,指定一些函数,编译阶段去除调用这些函数产生的返回值(如果这些返回值没有被使用的话),传console.log时会与drop_console产生相同效果,由于side-effect问题,                tree-shaking在这时不会生效
                        // pure_funcs: ["console.log"]
                    },
                    // 不保留注释
                    format: {
                      comments: false,
                    },
                }
            }),
            // 压缩css
            new OptimizeCSSPlugin({
                cssProcessorOptions: {
                    // 配置从语义上理解,不解释了
                    discardComments: {
                        removeAll: true,
                    },
                    canPrint: true
                }
            }),
        ],
        runtimeChunk: true,
        // splitChunks依托SplitChunksPlugin取代了webpack3及之前版本的CommonsChunkPlugin,配置思路大同小异。
        splitChunks: {
            hidePathInfo: true,
            cacheGroups: {
                vendor: {
                    name: "vendor",
                    chunks: "initial",
                    // priority默认是0,以0为基准决定处理bundle的优先级,值越大优先级越高。如果优先级分配不恰当,配置的效果可能不会特别理想。
                    priority: 0,
                    // 复用在main中已经包含了的模块。
                    reuseExistingChunk: true,
                    test: /node_modules\/(.*)\.js/,
                },
                commons: {
                    name: "commons",
                    chunks: "async",
                    priority: -10,
                    reuseExistingChunk: true,
                },
                // 在js分包配置之外,追加样式缓存组。
                styles: {
                  test: /\.css$/,
                  chunks: "all",
                  reuseExistingChunk: true,
                  enforce: true,
                  priority: 10,
                },

            },
            maxSize: 1000000,
        },
    }

  加上这段配置之后的构建表现:

css代码压缩效果:

  图片压缩

这一流程中我使用的是image-webpack-loader,这个loader主要依赖一个叫imagemin的第三方库,外接了一些库在node环境下做图片压缩,所以下载loader之后会自动下载这几个库,有没有必要使用这个loader我觉得看项目具体需要,对于追求极致的包体积和图片的加载速度还是有用处的,只是对构建时间影响比较大,image-webpack-loader国内装依赖比较麻烦,推荐用cnpm装。稍微有点需要吐槽的是,似乎都是一个团队出来的工具,为什么每个工具的配置参数还不一样……

  下面直接上代码:

wbepack.base.conf.js

// 从module中抽出处理图像的loader
const imageLoaders = {
    test: /\.(cur|png|jpe?g|gif|svg)(\?.*)?$/,
    use: [
      {
        loader: 'url-loader',
        query: {
          esModule: false,
          limit: 10,
          name: utils.assetsPath('img/[name].[ext]') 
        }
      },
    ]
};
if (process.env.NODE_ENV === "production" && config.build.imageCompress) {
  imageLoaders.use.push({
    loader: "image-webpack-loader", 
    options: {

      // 处理jpeg
      mozjpeg: {
        quality: 95,
        progressive: true, //官网原文是false creates baseline JPEG file. 不是搞图像的,不知道baseline意味着什么,就选了默认值true。
      },
      // gif
      gifsicle: {
        interlaced: true, 
      },
      // 将JPG和PNG图像压缩为WEBP,我这里的图基本全是png格式的,所以就没有对专门处理png图像的工具做配置,用webp一起处理了。
      webp: {
        quality: 85, // 图像品质
        method: 5, // 0-6 这个参数控制压缩速度、压缩后文件体积,当然也是跟图像品质挂钩的,具体细节不是特别清楚
      },
    },
  })
}

……
module: [
    ……
    // 在module中加入imageLoaders
    imageLoaders
]

看下对比:

  当然这个压缩的过程其实是比较耗时间的,所以在本地开发的时候我这边是不开启的。

  附:image-webpack-loader可配置的几个可配置项的配置参数列表:

  github.com/imagemin/im…

  github.com/imagemin/im…

  github.com/imagemin/im…

  github.com/imagemin/im…

  github.com/imagemin/im…

  多线程(慎用)

  这个操作对我来说一直都是都市传说,这回有机会亲手试一试,主要目的其实就是更充分利用算力,目前据我所知terser-webpack-plugin是默认开启了多线程的,这点读者可以在构建的时候打开任务管理器看到,除此之外构建流程中还存在其他耗时过长的工具需要处理,因为happypack已经很久没更新了,所以这里我在构建流程中选择加入了thread-loader,尝试缩减构建耗时。

  我在反复测试了几次之后才确定这个loader有点小坑,loader本身效果是存在一定争议的,这点在国内的各种教程或者帖子没怎么看到有人提出来,我通过google找到了一篇博客,里边里边有提到负优化的问题,使用了loader之后耗时更多了,知乎上的一个老哥也是做过测试的,0配置或者配置不对就负优化,配置对了构建时间也就能提升个一丝半点,切换配置跑了很多遍之后,我算是调整出了一份没有负优化的配置(当然正面优化也微乎其微,总体花的时间没什么大的波动),并且不能保证换到另外一个项目也能适用,这里放出来给读者参考一下:

多线程处理babel转译与图片压缩:

// webpack.prod.conf.js
const threadLoader = require("thread-loader")
threadLoader.warmup({
  poolTimeout: 1000,
  workerParallelJobs:50,
  poolParallelJobs:500
},["babel-loader"])
threadLoader.warmup({
  poolTimeout: 800,
  workerParallelJobs:50,
  poolParallelJobs:500
  // workers: 6,
},["image-webpack-loader"])
// 从module中抽出处理图像的loader
const imageLoaders = {
    test: /\.(cur|png|jpe?g|gif|svg)(\?.*)?$/,
    use: [
      {
        loader: 'url-loader',
        query: {
          esModule: false,
          limit: 10,
          name: utils.assetsPath('img/[name].[ext]') 
        }
      },
    ]
};
if (process.env.NODE_ENV === "production" && config.build.imageCompress) {

  imageLoaders.use = imageLoaders.use.concat([
    {
      loader: "thread-loader",
      options: {
        poolTimeout: 1000,
        workerParallelJobs:50,
        poolParallelJobs:500
      }
    },
    {
      loader: "image-webpack-loader", 
      options: {
        // 处理jpeg
        mozjpeg: {
          quality: 90,
          progressive: true, //官网原文是false creates baseline JPEG file. 不是搞图像的,不确定baseline意味着什么,就选了默认值true。
        },
        // gif
        gifsicle: {
          interlaced: true, 
        },
        // 将JPG和PNG图像压缩为WEBP,我这里的图基本全是png格式的,所以就没有对专门处理png图像的工具做配置,用webp一起处理了。
        webp: {
          quality: 85, // 图像品质
          method: 5, // 0-6 这个参数控制压缩速度、压缩后文件体积,当然也是跟图像品质挂钩的,具体细节不是特别清楚
        },
      },
    },
  ])
}
const scriptLoaders = {
  test: /\.js$/,
  use: [
    {
      loader: "thread-loader",
      options: {
        poolTimeout: 1000,
        workerParallelJobs:50,
        poolParallelJobs:500
      }
    },
    {
      loader: "babel-loader",
    },
  ],
  include: path.resolve(__dirname, "../src"),
  exclude: /node_modules/,
}

  我的项目构建时间因为image-webpack-loader从约110s到约150s(图片文件数量约为300),加入多线程的配置后构建时间平均在148s,前后的平均数值波动不是特别大,可你要说thread-loader没生效也不对,可以看到cpu前一分钟的使用率有一段(大概持续了半分钟,这段时间是image-webpack-loader在压缩文件)是明显提高了不少,但不知道为什么这点提升没有体现到构建时间上。需要提醒的是,因为多线程的原因,桌面会因为不停启动image-webpack-loader所依赖的node应用,导致鬼畜地一直弹框,影响正常工作,所以在本地构建的时候建议还是不要开启多线程使用image-webpack-loader了(或者锁屏休息一下)

  合理使用缓存

这次介绍cache-loader,用来缓存部分编译的结果,同样,你应该只在性能消耗较大的部分使用它。与thread-loader类似,在需要缓存的loader前加上cache-loader即可。

  在css代码处理过程中使用cache-loader:

// utils.js
……
……
……
  function generateLoaders (loader) {
    ……
    if (options.usePostcss){
      outputLoaders.push(postcssLoader)
    }
    // 经过测试发现在postcss-loader和mini-css-extract-plugin的loader之前插入cacheloader会报错,最终选择在postcss之后插入cache-laoder
    if (options.useCssCache) {
      outputLoaders.push({
        loader:"cache-loader"
      })
    }
    if (loader) {
      const targetloader = {loader:loader+"-loader"}
      outputLoaders.push(targetloader)
    }
    return outputLoaders
  }
……
……
……

utils.js

在图像压缩和babel转译过程中使用cache-loader:

// 从module中抽出处理图像的loader
const imageLoaders = {
    test: /\.(cur|png|jpe?g|gif|svg)(\?.*)?$/,
    use: [
      {
        loader: 'url-loader',
        query: {
          esModule: false,
          limit: 10,
          name: utils.assetsPath('img/[name].[ext]') 
        }
      },
    ]
};
if (process.env.NODE_ENV === "production" && config.build.imageCompress) {
  imageLoaders.use = imageLoaders.use.concat([
    {
      loader: "thread-loader",
      options: {
        poolTimeout: 800,
        workerParallelJobs:50,
        poolParallelJobs:500
      }
    },
    // 添加cache-loader
    {
      loader: "cache-loader",
    },
    {
      // image-webpack-loader……
    },
  ])
}
const scriptLoaders = {
  test: /\.js$/,
  use: [
    {
      loader: "thread-loader",
      options: {
        poolTimeout: 1000,
        workerParallelJobs:50,
        poolParallelJobs:500
      }
    },
    // 添加cache-loader
    {
      loader: "cache-loader",
    },
    {
      loader: "babel-loader",
      options: {
        // 如果只需要对babel-loader的处理结果进行缓存,把cacheDirectory设为true就可以了,这里因为使用了cache-loader,cacheDirectory就没有做配置,并且本地测试出来使用cache-loader比cacheDirectory=true在构建耗时上的成绩会更好
        // cacheDirectory: true,
      },
    },
  ],
  include: path.resolve(__dirname, "../src"),
  exclude: /node_modules/,
}

  thread-loader用不用效果都不怎么明显,还可能会翻车,但cache-loader就真正可以说是立竿见影了,你会看到在对一些性能消耗较大的loader的处理结果进行缓存之后,第二次及以后的构建时间有了明显的缩短(发出了反派一样邪恶的笑声):

  before:

  after:

  肥大 出 饰拳

  效果拔群

当然遗憾的是,如果自动构建开始的时候清掉node_modules目录重新装依赖的话,缓存也会……

  lint机制

  lint是一种针对静态代码的检测机制,用来在编译前检测出代码显式存在的一些问题,也被用来树立并且执行一套代码规范。lint检测的范围大概可以分为风格检查跟质量检查,打个比方,如果项目里设置了缩进的规则为两个空格,如果有一行代码中的缩进是4个空格,那么这个缩进问题就属于代码风格上的问题;如果你写了一个函数,return之后的行里还写了其他代码,这种必定不会执行的代码被lint机制检查出来时就可以归类为代码质量问题。这里我使用代码风格交给prettier控制,eslint主要对代码质量问题还有小部分代码风格问题进行了检查。

   eslint(在这之前你需要下载eslint与eslint-loader,在package.json中它们应该归类到devDependencies):

  我用的编辑器是vscode,所以这一部分我只介绍vscode和webpack上关于eslint的一些配置。

  首先在根目录下创建一个名为**.eslintrc**的配置文件(json)用来描述eslint使用到的工具、插件和具体的代码规则:

module.exports = {
    // 首先你需要确定检查范围,root默认值为true,从根目录开始
    root: true,
    // 指定运行环境
    env: {
      node: true,
      es6:true
    },
    // 设置语法选项和解析器
    parserOptions: {
        parser: "babel-eslint",
        "ecmaVersion": 6
    },
    // extends可以理解为继承某一套配置,recommended集成了eslint的核心规则,这里推荐只要是用eslint就要加上这个
    extends: ["eslint:recommended","plugin:vue/recommended", "@vue/prettier"],
    // 自选规则,根据需要配置,详细请看https://cn.eslint.org/docs/rules/
    rules: {
        "no-console": process.env.NODE_ENV === "production" ? "error" : "off",
        "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
        // Stylistic Issues
        "no-multi-spaces":["error", { ignoreEOLComments: false }],
        // ECMAScript 6
        "prefer-const": ['warn',{
          "destructuring": "any",
          "ignoreReadBeforeAssign": false
        }],
        "arrow-spacing": ['warn',{ "before": true, "after": true }],
      },
}

  你需要下载配置文件中提到的依赖,否则这些在构建过程中或者构建结束后会产生相应的报错或警告。

设置一个白名单.eslintignore:

dist
src/library
node_modules
static
// ...

  在webpack配置中添加eslint-loader(我这里只在本地开发环境下使用eslint,当然也可以在production模式下开启,就是构建耗时多一些,只是我认为本地环境下自己写的代码语法报错/警告不解决还敢提代码那就属于管理问题了)

webpack.base.conf.js

const createLintingRule = () => ({
  // 本来检查范围内包括JS文件,但因为目录中有些js文件为第三方库,在此不做解析,只在保存js文件时使用插件去格式化
  test: /\.(js|vue)$/,
  loader: "eslint-loader",
  enforce: "pre",
  // 可以通过include属性规定作用范围
  // include: /src/,
  options: {
    formatter: require("eslint-friendly-formatter"),
    emitWarning: !config.dev.showEslintErrorsInOverlay
  }
});

……
……
……
rules:[
    // config.dev.useEslint = true
    ...(config.dev.useEslint ? [createLintingRule()] : []),
……
……
……
]

prettier(需要下载依赖)  

  全部选项请看这里

  .prettierrc(json):

{
  "eslintIntegration": true,
  "singleQuote": false,
  "bracketSpacing": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "semi": true,
  "quoteProps": "as-needed"
}

  如果你需要编辑器替你提示出来你需要做什么/怎么改、或者设置保存自动修复,你还可以到vscode的拓展中下载eslint插件,eslint并提示会尝试修复可修复的问题(在cn.eslint.org/docs/rules/中会有特殊的图标标注出来);也可以通过设置npm命令"eslint --fix"来进行批量修复,只是有时候修复的结果会出乎你的预期,这可能会引发其他逻辑上的错误,所以如果你是项目开发的中途引入的eslint,要谨慎使用fix命令。

.vscode/settings.json

{
    // tab长度
    "editor.tabSize": 2,
    // 一行字符数量
    "editor.rulers": [120],
    // 行号
    "files.eol": "\n",
    "editor.lineNumbers": "on",
    // 代码提示
    "editor.snippetSuggestions": "top",
    // 保存自动修复
    "editor.codeActionsOnSave": {
        "source.fixAll": true,
    },
    // vetur配置
    "vetur.format.options.tabSize": 4,
    "vetur.format.scriptInitialIndent": false,
    "vetur.format.styleInitialIndent": true,
    "vetur.format.defaultFormatter.html": "prettyhtml",
    "vetur.format.defaultFormatter.js": "prettier",
    "vetur.format.defaultFormatterOptions": {
        "js-beautify-html": {
            "wrap_line_length": 120,
            "wrap_attributes": "auto"
        }
    },
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        "vue",
    ],
    "eslint.options": {
        "extensions": [".js",".vue"]
    }
}

  附:eslint在各种平台上的集成的入口

  本来还想写些锦上添花的优化比如美化控制台输出、端口防重(portfinder)的,但想了想其实这些东西本来也没什么门槛,而且不是每个人都需要这些东西,写在这里无疑让这篇本来就够水的文章更加乏味,所以打算就在这里结束。

  最近时间不是特别稳定(或者可以说特别稳定地少),但这篇博客确实是花了很多时间,主要是懒,还有怕被喷烂所以为了保证数据的真实性翻来覆地改配置然后跑项目,虽然我自己对这种便秘式的更新并不感到羞愧,但无论是写文章还好,写代码还好,我对这段时间自己的状态确实不满意,总之希望以后能够学习和分享更有意义的内容。