first thing fitrst 博主声明:绝对不当标题党
有人看最好不过的背景:

  十月初对公司产品的前端构建做了一些优化,但还遗留了不少问题(可了解我的前一篇博文:一次webpack小规模优化经历 https://www.cnblogs.com/byur/p/13977657.html),这里姑且列了一个表出来记录当前这个版本的不足:

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

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

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

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

  综合考虑以上问题后,个人判断webpack1的性能不足以为前端项目的构建流程提供更好的支持,遂决定把webpack升级到更高版本,用新特性与更强的性能,改善构建体验,造福运维与测试同事hhhh。

这次基本没图的正文:

  本系列博文将演示如何将webpackv1.x(1.13.2)升级到v4.x(4.44.2),选择这个小版本的原因是因为它是webpack4的最新一个小版本(2020.11),webpack4从发布测试版本到现在为止已经有两年多了,两年里的迭代和bug修复,足够让这个大版本的功能变得完善和稳定到让人信任的程度。至于升级的手法,我认为在原来配置的基础上做修改逐步升级,极有可能会被原来的写法误导,导致浪费时间,所以这次升级过程中我换了一种思路,具体的做法是做备份之后删除原来的配置文件,从零开始进行升级,因此本文兴许也可以当作一个用webpack构建项目的入门教程。

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

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

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

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

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

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

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

1 const path = require('path');
2 module.exports = {
3     entry: "./src/test.js",
4     output: {
5         path: path.resolve(__dirname,"dist"),
6         filename: 'testbundle.js',
7     }
8 }

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

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

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

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

WARNING in configuration
The '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: https://webpack.js.org/configuration/mode/

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

  刚才演示的配置文件,灵活性与性能都远远达不到真实工作场景的需求,只能称作玩具,所以接下来你将接触更具体也更接近实际场景的配置。

  现在,我们把配置文件改一改,常规的思路是将开发环境的配置跟产品环境的配置分离成两个文件,一般命名为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的基础之上改写配置文件,此时执行打包命令你将会发现控制台输出了很多错误,例如:

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

  

  满屏的红字有些吓人,但仔细看看就会发现其实并不是什么大不了的问题,截图上有一段样式代码,并且报错提示你可能需要其他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官网描述)

  const vueStyleLoader = {
    loader: "vue-style-loader"
  }
const cssLoader
= { loader: "css-loader",
  
// 如果你使用的是vue-style-loader并且css-loader的版本在v4.0.0及以上,这个属性需要加上,具体原因请看https://www.cnblogs.com/byur/p/14194672.html
   
   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)

基于vue2.x的webpack升级与项目搭建指南--基础篇-LMLPHP基于vue2.x的webpack升级与项目搭建指南--基础篇-LMLPHP
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.js

  

  把build命令改写为:

  

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

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

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

  过程没报错,一个基础的打包流程,到现在其实就走完了,这个包实际上也不能用,但我认为基础篇的意义在于引导读者顺利完成第一步,在这个前提之上进行功能的丰富,这样的话无论是操作失误回退代码或者是对优化方向的梳理都有一定的积极意义。

  基础篇到这里就该结束了,在进阶篇,我将展示一个完成度更高的版本。

  

  附:
基于vue2.x的webpack升级与项目搭建指南--基础篇-LMLPHP基于vue2.x的webpack升级与项目搭建指南--基础篇-LMLPHP
var path = require('path')
var config = require('../config')
var utils = require('./utils')
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
  mode: "none",
  entry: "./src/module/indexApp/index.js",
  output: {
    path: config.build.assetsRoot,
    publicPath: config.build.assetsPublicPath,
    filename: utils.assetsPath('js/[name][hash].js'),
    chunkFilename: utils.assetsPath('js/[id][chunkhash].js')
  },
  resolve: {
    extensions: ["*",'.js', '.vue'],
    alias: {
      'vue$': 'vue/dist/vue',
      'src': path.resolve(__dirname, '../src'),
      'common': path.resolve(__dirname, '../src/common'),
      'components': path.resolve(__dirname, '../src/components'),
      'components2': path.resolve(__dirname, '../src/components2'),
      'module': path.resolve(__dirname, '../src/module'),
      'config': path.resolve(__dirname, '../src/config'),
      'library': path.resolve(__dirname, '../src/library'),
      'jsplumb': path.resolve(__dirname, '../src/library/jsplumb.js'),
      'echarts-wordcloud': path.resolve(__dirname, '../src/library/echarts-wordcloud')
    }
  },
  module: {
    rules: [
      ...utils.styleLoaders(),
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.(cur|png|jpe?g|gif|svg)(\?.*)?$/,
        loader: 'url-loader',
        exclude: [
          path.resolve(__dirname, '../src/components/icon'),
        ],
        query: {
          limit: 10,
          name: utils.assetsPath('img/[name].[ext]')
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        loader: 'url-loader',
        query: {
          limit: 10000,
          name: utils.assetsPath('fonts/[name].[ext]')
        }
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ]
}
webpack.base.conf.js
基于vue2.x的webpack升级与项目搭建指南--基础篇-LMLPHP基于vue2.x的webpack升级与项目搭建指南--基础篇-LMLPHP
var path = require('path')
var config = require('../config')

exports.assetsPath = function (_path) {
  var assetsSubDirectory = process.env.NODE_ENV === 'production'
    ? config.build.assetsSubDirectory
    : config.dev.assetsSubDirectory;
  return path.posix.join(assetsSubDirectory, _path)
}

exports.cssLoaders = function () {
  const vueStyleLoader = {
    loader: "vue-style-loader"
  }
  const cssLoader = {
    loader: "css-loader",
  }
  // loader解析顺序从右至左
  // const baseLoaders = [vueStyleLoader,cssLoader]
  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
}
utils.js
基于vue2.x的webpack升级与项目搭建指南--基础篇-LMLPHP基于vue2.x的webpack升级与项目搭建指南--基础篇-LMLPHP
process.env.NODE_ENV = 'production'

var webpack = require('webpack')
var webpackConfig = require('./webpack.base.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.js

  

01-15 01:11