去骨鸡腿排

there's million I can see, only one shines for me

导航

基于vue2.x的webpack升级与项目搭建指南--进阶篇

 

在正式开始之前:

  本文将在上一篇的基础上进行拓展,渐进演化出一个相对于实际开发/生产环境切实可用的前端构建配置,具体涉及到的工具包括:

  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. thread-loader
  9. cache-loader
  10. eslint & eslint-loader

 

  之前介绍过了,webpack4的mode属性有"development"和"production"两种,对应开发环境与生产环境,由于两个环境下的配置会出现差异,一个webpack.base.conf就不太能适应接下来的应用场景了,所以接下来在追加配置之前,你也许需要分化出对应的配置文件,在不同的场景下加载不同的打包配置,这也是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做了追加、替换处理)

 

clean-webpack-plugin

  clean-webpack-plugin用来清空某个目录,如果不指定目录,则默认清空output.path中所指定的路径,虽然实现这个功能有不少途径,但为了方便,还是选择使用了这个在我来看是刚需的插件。现在刚刚介绍过的webpack-merge就体现除了作用——开发环境下(如果你使用的是webpack-dev-server)编译生成的文件是不会写入硬盘的,所以你应该在webpack.prod.conf.js中添加这个插件而不是webpack.base.conf.js。

  

const merge = require("webpack-merge").merge
const webapckBaseConfig = require("./webpack.base.conf")
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = merge(webapckBaseConfig,{
    mode:"production",
    plugins: [
        new CleanWebpackPlugin(),
    ]
})

 

html-webpack-plugin

  在上一篇文章的最后得到的dist目录的结构描述成json是这样的:

"dist":[

  "static": [

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

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

    {"js":[...]}

  ]
]

 

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

  This is a webpack plugin that simplifies creation of HTML files to serve your webpack bundles. This is especially useful for webpack bundles that include a hash in the filename which changes every compilation. You can either let the plugin generate an HTML file for you, supply your own template using lodash templates or use your own loader.)

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

  https://github.com/jantimon/html-webpack-plugin

 

const HtmlWebpackPlugin = require("html-webpack-plugin")
...
...
plugins: [
    ...
    new HtmlWebpackPlugin({
     // 选择一个本地的html文件作为模板而不是让插件自动生成
      template:"src/module/indexApp/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目录就成为了这样:

 

 细节:

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

 

babel-loader & babel

   babel用于将es6以及更前沿的语法转成es5语法以便于代码能在低版本环境中运行,使用babel转译到低级语法算是绝大多数项目在构建过程中普遍存在的流程,如果你的项目是从零开始构建而不是在老版本的babel配置基础上重新改的话,你在配置babel的时候也许会比后一种方式少花很多时间(我当初是用后一种方式慢慢摸索解决报错终于完成babel7.x的升级,这次复盘的时候换了一种思路,把旧版的配置与依赖全部删除之后直接上新版本以及配置babelrc文件,其过程顺利得让我怀疑之前辛苦爬过的坑像是幻觉一般没有真实存在过……)。

  话不多说,上代码。

  公司的项目之前使用的是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目录中的js文件时跳过 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
}
.babrlrc

 

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的功能,这种情况下在config/index.js中设置一个开关是不错的选择,添加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: {
        // 开启postcss需要在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文件中。

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则展示在浏览器中,建议用true。
        inline: 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,
    },
})  

  附:config.dev

  dev: {
    env: "development",
    port: 8090,
    host: "localhost",
    assetsSubDirectory: "static",
    assetsPublicPath: "/",
   // 你的私人配置 proxyTable: inter.proxy,
autoOpenBrowser: true, errorOverlay: true, // https://www.webpackjs.com/configuration/devtool/#devtool 需要慎重选择,会影响构建和重载速度 devtool: "#eval-source-map", usePostcss: false, },

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

/**
 * 写法参考:
 * 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计算出来的总耗时会有误差,一般来说webpack的统计信息中的耗时会比speed-measure-webpack-plugin会少,因为webpack-bundle-analyzer的耗时会被speed-measure-webpack-plugin统计。

  config.build

  build: {
    env: require("./prod.env"),
    index: path.resolve(__dirname, "../dist/index.html"),
    // 输出静态文件的目录
    assetsRoot: path.resolve(__dirname, "../dist"),
    // 除index外其他文件的目录
    assetsSubDirectory: "static",
    // 资源引用的公共前缀
    assetsPublicPath: "",
    productionSourceMap: false,
    devtool: "#source-map",
    usePostcss: true,
    // 手动开启,仅供调试分析用,改动该属性时禁止提交该部分代码
    analyzeMode: false,
  },

  webpack.prod.conf.js

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')
const webpackProdConfig = merge(webapckBaseConfig,{
    mode:"production",
    module: {
        // 开启postcss
        rules: utils.styleLoaders({
            usePostcss:config.build.usePostcss
        }),
    },
    devtool: config.build.productionSourceMap?config.build.devtool:false,
    plugins: [
        new CleanWebpackPlugin(),
    ]
})
// 增加一层判断
if (config.build.analyzeMode){
    const WebpackBundleAnalyzer = require("webpack-bundle-analyzer").BundleAnalyzerPlugin
    const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
    const spw = new SpeedMeasurePlugin()
    webpackProdConfig.plugins.push(new WebpackBundleAnalyzer())
    module.exports = spw.wrap(webpackProdConfig);
    return;
}
module.exports = webpackProdConfig;

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

    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
}
utils.js
   webpack.prod.conf.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.conf.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可配置的几个可配置项的配置参数列表:

     https://github.com/imagemin/imagemin-mozjpeg#options

  https://github.com/imagemin/imagemin-opti

  https://github.com/imagemin/imagemin-pngquant

  https://github.com/imagemin/imagemin-svgo

  https://github.com/imagemin/imagemin-gifsicle

 

  多线程(慎用)

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

  这里我截取了一部分thread-loader在webpack官方文档上的描述,用来凑字数(不是)

Put this loader in front of other loaders. The following loaders run in a worker pool.
// 将thread-loader放置在其他loader之前
Loaders running in a worker pool are limited. Examples:

Loaders cannot emit files.
Loaders cannot use custom loader API (i. e. by plugins).
Loaders cannot access the webpack options.
// 这些loader存在以下限制:
// 这些loader不能生成额外的文件
// 不能使用自定义的loader,比如某个插件配套的loader,比如mini-css-extract-plugin的loader?
// 不能获取webpack的配置项

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

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

  webpack.base.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
……
……
……
  function generateLoaders (loader) {
    const outputLoaders = [vueStyleLoader,cssLoader]

    if (options.extractCss){
      outputLoaders.splice(1,0,{
        loader:MiniCssExtractPluginLoader,
        options: {
          publicPath: "../../",
        },
      })
    }
    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",
    },
    {
      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
      }
    },
    // 添加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/,
}
webpack.base.conf.js

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

  使用前

  为image-webpack-loader和babel-loader加入缓存后:

  在样式解析过程中加入缓存后:

 

  肥大 出 饰拳

  效果拔群

 

  lint机制

  lint是一种针对静态代码的检测机制,用来在编译前检测出代码显式存在的一些问题,也被用来树立并且执行一套代码规范。lint检测的范围大概可以分为风格检查跟质量检查,打个比方,如果项目里设置了缩进的规则为两个空格,如果有一行代码中的缩进是4个空格,那么这个缩进问题就属于代码风格上的问题;如果你写了一个函数,return之后的行里还写了其他代码,这种必定不会执行的代码被lint机制检查出来时就可以归类为代码质量问题。这里我使用eslint主要对代码质量问题还有小部分代码风格问题进行了检查,大部分的代码风格检查转交prettier,用来避免可能会发生的规则冲突(在vue单文件组件中写代码时可能会遇到两种规则冲突导致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,本地环境下自己写的代码语法报错/警告不解决还敢提代码那就属于态度问题了)

   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会尝试修复可修复的问题(在https://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在各种平台上的集成的入口:https://eslint.org/docs/user-guide/integrations

 

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

  之前计划的时候还有一篇草稿叫~-毅力篇,因为第一次做的时候思路不太清晰,遇到了很多很多很多问题,本来起名为毅力篇是想用来记录报错的解决方案的,但写这篇文章复盘升级操作的时候没有历史包袱,从零开始,没想到过程还是比较顺利的,所以毅力篇的草稿估计是没有放出来的必要了。

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

 

posted on 2021-01-14 16:07  去骨鸡腿排  阅读(950)  评论(0编辑  收藏  举报