实现vue-cli(二):webpack实现项目打包

一、大概思路

(一)开发阶段的打包构建
  1. 配置打包的入口文件和输出目录等信息。
  2. 清空构建目录旧文件。处理js文件。
  3. 处理打包js/css/vue/图片字体等。
  4. 将打包结果注入html。
  5. 启动服务器预览页面,并监听变化。
  6. 增加eslint检测。
  7. 配置sourceMap等信息。
(二)发布阶段的打包构建
  1. 配置打包的入口文件和输出目录等信息。
  2. 清空构建目录旧文件。处理js文件。
  3. 处理打包js/css/vue/图片字体等(发布阶段还需要对css/js进行压缩混淆)。
  4. 拷贝public资源。
  5. 将打包结果注入html。
  6. 增加eslint检测。
  7. 配置sourceMap等信息。

二、“开发阶段打包构建”具体实现

(一)准备工作
  1. 请确保自己本地已有npm或yarn等包管理工具,本文用npm做演示。
  2. 下载该演示项目,或者自行vue-cli创建一个项目。
  3. 在该文件夹下空白处“按shift+鼠标右键”,选中“在此处打开命令行/powershell窗口”打开命令行窗口,或者自行通过命令窗口cd到该文件目录下。
  4. 命令行输入npm init回车,自行填写信息一路回车,最后生成package.json配置文件。
  5. 命令行输入npm install webpack webpack-cli -D安装webpack打包构建工具(注意:版本不一致可能会导致报错,本篇演示用的版本是"webpack": "5.36.2""webpack-cli": "3.3.12",详细版本见最后附录)。
  6. 在示例项目下新建webpack.config.js作为webpack的配置文件。
(二)配置打包入口和输出等信息

在webpack.config.js中添加以下代码,命令行输入npx webpack运行,默认会去执行webpack.config.js文件,如果成功生成temp/main.js文件则成功(会有报错后面解决)。

const path = require('path')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: '[name]_[contenthash:8].js', // 生成文件的名字
    path: path.join(__dirname, 'temp') // 生成文件放在哪,output.path必须是绝对路径
  },
  mode: 'none', // 设置webpack运行模式,有production/development/none三种取值,不同模式会内置不同功能
}
(三)清空输出目录的旧文件

如果多运行几次打包命令npx webpack就会发现,生成的index.js越来越多,而我们用到的其实只有最新的那个。为了避免冗余,我们可以使用插件在每次打包之前先删除下目录里的旧文件。npm install clean-webpack-plugin -D安装删除文件的插件,然后增加plugins配置。配置完后再执行打包npx webpack则只会保留最新的文件。

const path = require('path')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: '[name]_[contenthash:8].js', // 生成文件的名字
    path: path.join(__dirname, 'temp') // 生成文件放在哪,output.path必须是绝对路径
  },
  mode: 'none', // 设置webpack运行模式,有production/development/none三种取值,不同模式会内置不同功能
  plugins: [
    new CleanWebpackPlugin(),
  ],
}
(四)添加loader处理css/js/vue/图片/字体
  1. 处理css:由于webpack默认只对js进行打包处理,所以要单独对css进行处理。安装css处理所需要的loader,npm install style-loader css-loader less less-loader -D。其中css-loader只会处理css,并不会将css代码嵌入到最后打包内容里,需要再用style-loader将css嵌入到style里。
  • 多个loader时执行顺序是从下到上依次执行,所以反过来先写style-loader再css-loader(先处理再将结果插入,若有less要先转成css再用css-loader处理)。
  1. 处理js:webpack对js的处理只是打包合并,之所以能解析import export也是对模块化做了支持,对于一些es6新语法特性还需要用babel进行转化。npm install babel-loader @babel/core @babel/preset-env -D安装相关依赖,记得设置@babel/preset-env才能转码成功,因为转码插件都放在这里面。
  2. 处理vue: 安装vue处理需要的loader和依赖,npm install vue vue-loader vue-template-compiler -D,vue-loader还要搭配插件VueLoaderPlugin使用。
  3. 处理字体和图片:安装依赖npm install file-loader url-loader -D。一般较小的资源可以用url-loader转为base64进行加载,当资源较大base64会影响打包体积影响运行速度,所以超过大小用file-loader加载对应路径资源。
  • 注意:在file-loader v4.3.0版本之后默认使用了esModule语法,所以会导致图片的路径变成[object module]。解决方案可以修改图片的引用方式,更简单的直接给loader设置esModule: false
const path = require('path')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: '[name]_[contenthash:8].js', // 生成文件的名字
    path: path.join(__dirname, 'temp') // 生成文件放在哪,output.path必须是绝对路径
  },
  mode: 'none', // 设置webpack运行模式,有production/development/none三种取值,不同模式会内置不同功能
  module: {
    rules: [
      {
        test: /\.vue$/i,
        use: 'vue-loader',
      },
      {
        test: /\.js$/i,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env'],
            ],
          },
        },
      },
      {
        test: /\.(css|less)$/i,
        use: [
          'style-loader',
          'css-loader',
          'less-loader',
        ],
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/i,
        use: {
          loader: 'url-loader', // 用url-loader将较小的资源转出base64加载,较大的会影响打包体积影响运行速度
          options: {
            limit: 10 * 1024, // 超过10kb大小用file-loader加载
            name: 'img/[name].[contenthash:8].[ext]', // 指定file-loader处理生成路径名字
            esModule: false, // 解决图片路径变成[object module]的问题
          },
        },
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
        use: {
          loader: 'url-loader', // 用url-loader将较小的资源转出base64加载,较大的会影响打包体积影响运行速度
          options: {
            limit: 10 * 1024, // 超过10kb大小用file-loader加载
            name: 'font/[name]_[contenthash:8].[ext]', // 指定file-loader处理生成路径名字
            esModule: false, // 解决字体路径变成[object module]的问题
          },
        },
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new VueLoaderPlugin(),
  ],
}

经过上述处理,再运行npx webpack此时已经没有报错了。接下来我们就将打包出来的结果注入到html中并启动服务器预览看是否正常。

(五)将打包结果注入html
  • npm install html-webpack-plugin -D安装webpack处理html的插件。plugin和loader是webpack两个核心。loader一般用于对文件内容进行编译,并将处理结果直接插入到编译后的文件。而plugin可以对文件进行增删改等其他loader做不到的事情,比如删除和拷贝文件。
  • webpack中还提供了definePlugin可以用于为全局注入变量,然后在html等文件中访问该变量,值必须是可运行的js语句,即eval(变量值)不报错,所以如果值是字符串得加双引号否则认为是变量会报错。
    依据此可以实现不同运行环境下加入不同值,比如mode: 'production'时,会默认注入process.env.NODE_ENV = 'production'用于判断当前运行环境。但上面我们设置了mode: 'none'所以要自己注入这个变量。
const HtmlWebpackPlugin = require('html-webpack-plugin')
const Webpack = require('webpack')

module.exports = {
  ...,
  plugins: [
    new CleanWebpackPlugin(),
    new VueLoaderPlugin(),
    new Webpack.DefinePlugin({
      BASE_URL: '"../public/"', // 必须是可运行的js语句,即eval(BASE_URL)不报错,所以此处得加双引号否则认为是变量会报错
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV) || '"development"', // 直接读取node环境中的变量,如果未定义则设置为development
      },
    }),
    new HtmlWebpackPlugin({
      title: '测试标题', // 打包后html里的title
      filename: 'index.html', // 打包后的文件名
      template: './public/index.html', // 依据哪个模板文件来生成最后的html
    }),
  ],
}

增加上述代码之后命令行输入npx webpack打包,会生成temp下对应文件,浏览器手动打开temp/index.html,如果页面显示正常即成功。

(六)服务器启动并自动刷新
  • 运行webpack的时候加个--watch,能监听文件变化自动重新打包但需要手动刷新,可以用browser-sync temp --file "**/*"实现自动刷新。但由于又要写入磁盘再读出磁盘,会比较慢。
  • webpack-dev-server能自动运行打包编译和监听刷新,将打包内容写入缓存而不是磁盘,减少了磁盘读写。默认会将构建输出的资源作为加载文件,如果没构建的资源要自己配置额外资源路径。安装插件npm install webpack-dev-server -D,并增加以下配置。
  • 由于自动刷新整个页面会丢失原操作和文本,但有时我们希望保留原来输入框的内容只变更改动的地方,此时就需要热更新。模块热更新HMR(hot module replacement)可以实时替换改变的模块但不影响整体运行状态。通过设置hot: true可以开启热更新。
module.exports = {
  ...,
  devServer: {
    contentBase: ['public', '.'], // 额外指定找不到的资源(比如public里没打包到temp的静态资源)去哪里找
    port: 8080,
    open: true, // 是否自动启动浏览器
    // hot: true, // 如果hot处理代码有报错,则仍然会自动刷新整个页面
    hotOnly: true, // 只做热更新,无论有无报错也不自动刷新整个页面
  },
}

如果用的是webpack-dev-server4,已经取消了hotOnly和contentBase,可以将配置改成如下

module.exports = {
  ...,
  devServer: {
    static: {
      directory: './public'
    }, // 额外指定找不到的资源(比如public里没打包到temp的静态资源)去哪里找
    port: 8080,
    open: true, // 是否自动启动浏览器
    hot: true, // 如果hot处理代码有报错,则仍然会自动刷新整个页面
  },
}

命令行运行npx webpack-dev-server启动服务器,修改文件会自动编译刷新则代表成功。若出现Cannot find module 'webpack-cli/bin/config-yargs'报错,要修改webpack-cli版本,因为webpack-cli早在4.0版本后就移除了yargs,版本兼容没做好,可以在命令行输入npm install webpack-cli@3 -D将webpack-cli还原到3.3.12版本的版本,再次执行npx webpack-dev-server就可以了。

(七)sourceMap配置错误定位

由于现在运行的文件是经过编译的,跟我们开发时写的代码差异比较大,如果有报错信息只能定位到编译后的代码上,而无法定位到我们开发的具体代码位置,这点不利于我们调试和处理问题。source map源码地图指的是编译后代码和源码的对应关系,解决编译之后无法查看的问题。webpack提供了devtool可以让我们配置sourceMap模式。

常用的sourceMap模式有eval-source-map、cheap-eval-source-map、cheap-module-eval-source-map等。eval-source-map生成了source-map文件能定位问题行和列cheap-eval-source-map生成了简单source-map文件只能定位cheap-module-eval-source-map是定位编译前文件所在。即带cheap不能定位列信息;带module是和没loader加工过的源代码一模一样。

开发阶段建议使用cheap-module-eval-source-map,因为一般每行不超过80不需要定位列,且需要看源代码,虽然要定位编译前文件所在行处理多启动慢,但只有第一次慢后面修改不再重复处理不会慢。发布阶段建议用none,即不暴露源代码,因为一般应该在开发阶段就把错误调试处理完毕。实在担心上线后有报错要调试,可以用nosources-source-map只提供错误的行列信息但显示给用户空白代码,这样再到自己源代码里找行列就可以定位到错误位置。

module.exports = {
  ...,
  devtool: 'eval-cheap-module-source-map', // 此处写法和官方文档不一样,因为webpack5之后的devtool检测规则是^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$
}
(八)增加eslint检测

eslint主要用于检测代码风格和语法是否符合规范,能让团队的代码保持统一规范方便维护。命令行输入npm install eslint安装eslint,然后npx eslint --init生成eslint配置文件。npm install eslint-loader -D安装对应loader再增加下面loader配置。

{
  test: /\.(js|vue)$/i,
  exclude: '/node_modules/', // node_modules下的文件不需要eslint检测
  use: 'eslint-loader',
  enforce: 'pre', // 强制最先执行这个loader
},

此时在main.js里写一些语法错误,运行npx webpack-dev-server会看到eslint报错即代表成功。下面报错表示在main.js文件的第1行第22列缺少了分号。

D:\project\test\webpack_example\src\main.js
   1:22  error  Missing semicolon                                                          semi

如果觉得有些检测不符合团队的开发习惯,可以前往刚才init生成的.eslinterc配置文件里修改rules。具体的配置规则可以前往"https://eslint.cn/docs/rules/关键字"查看,比如上面的关键词是最后的semi(表示是否最后加分号),可以前往"https://eslint.cn/docs/rules/semi"查看修改配置规则。

// 修改.eslintrc
{
    "rules": {
        "semi": ["error", "never"], // 不需要分号结尾
        "linebreak-style": ["error", "windows"] // 采用windows的CRLF换行
    }
}

至此,开发阶段的webpack打包构建已经配置完毕。

三、“发布阶段打包构建”具体实现

发布阶段的构建大致跟开发阶段一样,但由于发布的内容是面向客户的,有一些要做优化。主要区别在以下几点:

  1. 不需要启动服务器调试。
  2. 构建的输出目录不同,sourceMap等配置不同。
  3. 由于无法访问本地资源,需要将public等不编译资源一起拷贝到输出目录。
  4. 为避免源码泄露和文件过大加载过慢,css等资源要进行压缩混淆处理,而不是简单地注入。
(一)准备工作

由于开发阶段和发布阶段有很多类似操作,所以我们可以将部分公共配置抽离出来复用,然后通过不同变量和命令来执行不同的打包操作。

  1. 新建webpack.common.js、webpack.dev.js、webpack.prod.js三个文件,分别用于存放公共配置、开发阶段配置、发布阶段配置。
  2. 将之前的webpack.config.js的内容复制到webpack.common.js里,然后webpack.dev.js先直接简单引入导出。
const common = require('./webpack.common.js')

module.exports = common
  1. npm install cross-env -D安装兼容设置环境变量的库(window和mac不兼容NODE_ENV=development这样设置变量),然后在package.json里的script增加以下配置(即先设置当前环境变量,再根据对应配置文件进行打包):
"scripts": {
    ...,
    "dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.dev.js",
    "build": "cross-env NODE_ENV=production webpack --config webpack.prod.js"
  },
  1. 命令行输入npm run dev,则会直接运行package.json中scripts.dev的命令,如果能跟之前一样正常打开页面则成功。
(二)抽离devServer

首先npm install webpack-merge -D安装webpack中专门用于合并配置信息的库(比Object.assign多做了一些特殊处理),然后将webpack.common.js文件里的devServer剪切到webpack.dev.js里。再次运行npm run dev看是否正常。

// webpack.dev.js
const common = require('./webpack.common.js')
const {merge} = require('webpack-merge') // 版本不一样,有些是直接merge = 有些要{merge} =

module.exports = merge(common, {
  devServer: {
    contentBase: ['public', '.'], // 额外指定找不到的资源(比如public里没打包到dist的静态资源)去哪里找
    port: 8080,
    open: true, // 是否自动启动浏览器
    // hot: true, // 如果hot处理代码有报错,则仍然会自动刷新整个页面
    hotOnly: true, // 只做热更新,无论有无报错也不自动刷新整个页面
    // overlay: { // 这里配置 html 页面是否显示 eslint 错误信息蒙版 
    //   errors: true,
    //   warnings: true,
    // },
  },
})
(三)根据环境变量配置不同内容

新建webpack_config.js文件(记得是_不是.,因为webpack.config.js是webpack的默认运行文件),写入以下内容:

const path = require('path')
module.exports = {
  dev: {
    mode: 'development',
    outputPath: path.join(__dirname, 'temp'),
    devtool: 'eval-cheap-module-source-map',
  },
  build: {
    mode: 'production',
    outputPath: path.join(__dirname, 'dist'),
    devtool: 'nosources-source-map',
  },
}

更改webpack.common.js的以下四处内容,再次运行npm run dev跟之前一样正常打开页面则成功。

let config = require('./webpack_config.js')
config = process.env.NODE_ENV === 'production' ? config.build : config.dev

module.exports = {
  ...,
  output: {
    filename: '[name]_[contenthash:8].js', // 生成文件的名字
    path: config.outputPath, // 生成文件放在哪,output.path必须是绝对路径
  },
  mode: 'none', // 此处修改为config.mode,为了方便演示暂时还用none
  devtool: config.devtool,
  plugins: [
    ...,
    new Webpack.DefinePlugin({
      ...,
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV || config.env), // 要是eval能运行的语句,所以用JSON.stringify包裹字符串
      },
    }),
  ],
}
(四)拷贝public下的资源

npm install copy-webpack-plugin -D安装插件,在webpack.prod.js写入以下内容。然后命令行输入npm run build打包,看到生成dist目录下各文件,点击index.html页面正常打开即代表成功。

const common = require('./webpack.common.js')
const {merge} = require('webpack-merge')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = merge(common, {
  plugins: [
    new CopyWebpackPlugin({patterns: [{from: 'public', to: 'public'}]}), // 拷贝public到对应目录,旧版本是new CopyWebpackPlugin(['public'])
  ],
})
(五)压缩混淆资源
  1. 之前我们的css都是直接通过style-loader注入的,这样不方便进行专门的css处理、按需加载和HMR热更新。我们可以用mini-css-extract-plugin来将css提取到单独的文件中,npm install mini-css-extract-plugin -D安装插件,更改以下文件内容:
// webpack_config.js
module.exports = {
  dev: {
    ...,
    extract: false,
  },
  build: {
    ...,
    extract: true,
  },
}
// webpack.common.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  module: {
    rules: [
      {
        test: /\.(css|less)$/i,
        use: [
          config.extract ? MiniCssExtractPlugin.loader : 'style-loader',
          'css-loader',
          'less-loader',
        ],
      },
    ],
  },
  plugins: [
    ...,
  ]
  .concat(config.extract ? [new MiniCssExtractPlugin({filename: '[name]_[contenthash:8].css'})] : []),
}
  1. 上面将css单独提取出来后,我们就可以对css和js进行专门的处理压缩了。其实webpack本身就会对js的打包进行一些优化处理,但其他资源需要我们自行处理。npm install optimize-css-assets-webpack-plugin -D安装压缩css的插件,正常也是将该插件放在plugin选项,但官方建议放在optimization里的minimizer中,这样可以统一控制是否要开启压缩,比如mode: 'production'会自动开启minimizer
  • 注意:当我们配置了minimizer此项后,会导致webpack认为我们要自定义处理,则不会再自动压缩js,所以需要npm install terser-webpack-plugin -D然后手动添加js压缩插件terser-webpack-plugin
// webpack.prod.js增加下面内容
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserWebpackPlugin(), // js压缩插件
      new OptimizeCssAssetsWebpackPlugin(),
    ],
  },
}

至此,发布阶段的webpack打包构建也已经配置完成。更多定制化的功能可以前往webpack官网查看。

四、附录

最后,附上本篇演示使用的各个包的版本号。如果操作中遇到版本不兼容问题,可以尝试使用下方的版本:

{
  "devDependencies": {
    "@babel/core": "^7.14.0",
    "@babel/preset-env": "^7.14.1",
    "babel-loader": "^8.2.2",
    "clean-webpack-plugin": "^4.0.0-alpha.0",
    "copy-webpack-plugin": "^8.1.1",
    "cross-env": "^7.0.3",
    "css-loader": "^5.2.4",
    "eslint": "^7.26.0",
    "eslint-config-airbnb-base": "^14.2.1",
    "eslint-friendly-formatter": "^4.0.1",
    "eslint-loader": "^4.0.2",
    "eslint-plugin-html": "^6.1.2",
    "eslint-plugin-import": "^2.22.1",
    "eslint-plugin-vue": "^7.9.0",
    "eslint-plugin-vue-libs": "^4.0.0",
    "file-loader": "^6.2.0",
    "html-webpack-plugin": "^5.3.1",
    "less": "^4.1.1",
    "less-loader": "^8.1.1",
    "mini-css-extract-plugin": "^1.6.0",
    "optimize-css-assets-webpack-plugin": "^5.0.4",
    "style-loader": "^2.0.0",
    "terser-webpack-plugin": "^5.1.1",
    "url-loader": "^4.1.1",
    "vue-loader": "^15.9.6",
    "vue-style-loader": "^4.1.3",
    "vue-template-compiler": "^2.6.12",
    "webpack": "^5.36.2",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.2",
    "webpack-merge": "^5.7.3"
  },
  "dependencies": {
    "vue": "^2.6.12"
  }
}

你可能感兴趣的:(实现vue-cli(二):webpack实现项目打包)