Webpack4 搭建 Vue 專案

suporka發表於2018-08-22

1. 前言

由於 Parcel 打包工具的影響,webpack4 也追求零配置搭建專案。而前陣子出現的 vue-cli 3.0也是基於 webpack4 零配置的思想建立的。對於一些習慣webpack3 的開發者難免有些不習慣。本文就帶你繞過 vue-cli,用 webpack4 一步步搭建 vue 專案。

注:(本文講述的是webpack4基礎配置,文章有點長,請耐心看完。或者直接檢視專案原始碼,或者ctrl + w

2. 專案搭建

  1. 建立 createVue 資料夾,進入該資料夾, npm init 初始化專案

  2. 安裝 webpack 四件套

npm i webpack webpack-cli webpack-dev-server webpack-merge --save-dev

// 當前我使用版本
"webpack": "^4.16.3",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.5", // 開發伺服器
"webpack-merge": "^4.1.4" // webpack 配置合併
複製程式碼
  1. 建立相應檔案
createVue
  |--dist
  |--build
      |--webpack.prod.js
      |--webpack.dev.js
      |--webpack.base.js
  |--src
      |--index.js
      |--app.vue
  |--index.html
複製程式碼
// webpack.base.js
// 存放 dev 和 prod 通用配置
const webpack = require('webpack');

module.exports = {
  entry: './src/index.js', //入口
  module: {
    rules: []
  },
  plugins: [
    // 解決vender後面的hash每次都改變
    new webpack.HashedModuleIdsPlugin(),
  ],// 外掛
};
複製程式碼
// webpack.dev.js
// 存放 dev 配置
const merge = require('webpack-merge');
const common = require('./webpack.base.js');
const path = require('path');

module.exports = merge(common, {
  devtool: 'inline-source-map',
  devServer: { // 開發伺服器
    contentBase: '../dist'
  },
  output: { // 輸出
    filename: 'js/[name].[hash].js', // 每次儲存 hash 都變化
    path: path.resolve(__dirname, '../dist')
  },
  module: {},
  mode: 'development',
});
複製程式碼
// webpack.prod.js
// 存放 prod 配置
const path = require('path');
// 合併配置檔案
const merge = require('webpack-merge');
const common = require('./webpack.base.js');

module.exports = merge(common, {
  module: {},
  plugins: [],
  mode: 'production',
  output: {
    filename: 'js/[name].[contenthash].js', //contenthash 若檔案內容無變化,則contenthash 名稱不變
    path: path.resolve(__dirname, '../dist')
  },
});
複製程式碼

webpack4 增加了 mode 屬性,設定為 development / production,以下是預設配置

development:

process.env.NODE_ENV 的值設為 development
預設開啟以下外掛,充分利用了持久化快取。參考基於 webpack 的持久化快取方案

NamedChunksPlugin :以名稱固化 chunk id
NamedModulesPlugin :以名稱固化 module id

production:

process.env.NODE_ENV 的值設為 production
預設開啟以下外掛,其中 SideEffectsFlagPlugin 和 UglifyJsPlugin 用於 tree-shaking


FlagDependencyUsagePlugin :編譯時標記依賴
FlagIncludedChunksPlugin :標記子chunks,防子chunks多次載入
ModuleConcatenationPlugin :作用域提升(scope hosting),預編譯功能,提升或者預編譯所有模組到一個閉包中,提升程式碼在瀏覽器中的執行速度
NoEmitOnErrorsPlugin :在輸出階段時,遇到編譯錯誤跳過
OccurrenceOrderPlugin :給經常使用的ids更短的值
SideEffectsFlagPlugin :識別 package.json 或者 module.rules 的 sideEffects 標誌(純的 ES2015 模組),安全地刪除未用到的 export 匯出
UglifyJsPlugin :刪除未引用程式碼,並壓縮
複製程式碼
// index.js
// 需 npm i vue --save
import Vue from 'vue';
import App from './App.vue'
import './index.scss'
new Vue({
  el: '#app',
  render: h => h(App),
});
複製程式碼
<!-- app.vue -->
<template>
  <div id="app">
    hello world
  </div>
</template>

<script>
export default {
  name: 'app'
}
</script>

<style scoped>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
  transform: rotate(0deg);
}
</style>

複製程式碼
<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Suporka Vue App</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
複製程式碼
  1. 安裝 vue 核心解析外掛

npm i vue-loader vue-template-compiler --save-dev

// 當前我使用版本
"vue-loader": "^15.2.6",
"vue-template-compiler": "^2.5.17",
複製程式碼

由於 vue 的解析在 dev 和 prod 中均需使用,因此歸入基本配置 base

// webpack.base.js

// ...省略號
// vue-loader 外掛
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
  //...省略號
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  plugins: [
    // 請確保引入這個外掛來施展魔法
    new VueLoaderPlugin(),
  ]
};
複製程式碼
  1. 安裝 html 模板解析外掛

npm i html-webpack-plugin --save-dev

// 當前版本 
"html-webpack-plugin": "^3.2.0"
複製程式碼

html 解析也屬於基本配置,歸入 base

// webpack.base.js

// ...省略號
// html外掛
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  //...省略號
  plugins: [
    //...省略號
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../index.html'),
    }),
  ]
};
複製程式碼
  1. 建立 npm 命令
"scripts": {
  "start": "webpack-dev-server --hot --open --config build/webpack.dev.js",
  "build": "webpack --config build/webpack.prod.js"
},
複製程式碼

--hot 模組熱替換

--open 開啟本地伺服器

此時 npm start,專案可正常執行

3. 功能擴充

  1. 新增 loader
  • CSS loader (包括前處理和後處理)

CSS 基礎 loader

"css-loader": "^1.0.0",
"style-loader": "^0.21.0",
複製程式碼

CSS 前處理 less 兩件套

"less": "^3.8.0",
"less-loader": "^4.1.0",
複製程式碼

CSS 前處理 sass 兩件套

"node-sass": "^4.9.2",
"sass-loader": "^7.1.0",
複製程式碼

CSS 後處理 postcss 兩件套

"postcss-loader": "^2.1.6",
"autoprefixer": "^9.1.0",
複製程式碼

並在根資料夾建立 postcss.config.js 檔案

// postcss.config.js
// 自動新增css相容屬性
module.exports = {
  plugins: [
    require('autoprefixer')
  ]
}
複製程式碼

安裝以上依賴,在 base 檔案中加入一下 loader 程式碼

// webpack.base.js

// ...省略號
rules: [
  {
    test: /\.(sa|sc|c)ss$/,
    use: [
      'style-loader',
      'css-loader',
      'postcss-loader',
      'sass-loader',
    ],
  },
  {
    test: /\.less$/,
    use: [
      'style-loader',
      'css-loader',
      'postcss-loader',
      'less-loader',
    ],
  },
]
複製程式碼
  • 圖片 loader

解析圖片,字型等都是用 file-loader,安裝npm i file-loader --save-dev

base 檔案加入配置

// webpack.base.js

// ...省略號
rules: [
  // ...省略號
  {
    test: /\.(png|svg|jpg|gif)$/,
    use: [
      {
        loader: 'file-loader',
        options: {
          limit: 5000,
          // 分離圖片至imgs資料夾
          name: "imgs/[name].[ext]",
        }
      },
    ]
  },
]
複製程式碼

4. 打包優化

  1. 解決每次重新打包,dist 資料夾檔案未清除
  • 安裝 clean-webpack-plugin 外掛
// webpack.prod.js

// 打包之前清除檔案
const CleanWebpackPlugin = require('clean-webpack-plugin');
// ...省略號
plugins: [
  new CleanWebpackPlugin(['dist/*'], {
    root: path.resolve(__dirname, '../')
  }),
]
複製程式碼
  1. 分離 CSS

webpack4 中使用 mini-css-extract-plugin 外掛來分離 css。

  • 安裝 mini-css-extract-plugin 外掛後
// webpack.prod.js

// 分離CSS外掛
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// ...省略號
plugins: [
  new MiniCssExtractPlugin({
    filename: "css/[name].[hash].css",
    chunkFilename: 'css/[id].[hash].css'
  }),
]
複製程式碼

另外,還需將各個 css loader中的style-loader 替換為 MiniCssExtractPlugin

圖片壓縮使用 image-webpack-loader, 安裝後 程式碼如下:

// webpack.prod.js
// ...省略號
rules: [
  {
    test: /\.(sa|sc|c)ss$/,
    use: [
      {
        loader: MiniCssExtractPlugin.loader,
        options: {
          // you can specify a publicPath here
          // by default it use publicPath in webpackOptions.output
          publicPath: '../'
        }
      },
      'css-loader',
      'postcss-loader',
      'sass-loader',
    ],
  },
  {
    test: /\.less$/,
    use: [
      {
        loader: MiniCssExtractPlugin.loader,
        options: {
          // you can specify a publicPath here
          // by default it use publicPath in webpackOptions.output
          publicPath: '../'
        }
      },
      'css-loader',
      'postcss-loader',
      'less-loader',
    ],
  },
  {
    test: /\.(png|svg|jpg|gif)$/,
    use: [
      {
        loader: 'file-loader',
        options: {
          limit: 5000,
          name: "imgs/[hash].[ext]",
        }
      },
      // 圖片壓縮
      {
        loader: 'image-webpack-loader',
        options: {
          //   bypassOnDebug: true,
          mozjpeg: {
            progressive: true,
            quality: 65
          },
          optipng: {
            enabled: false,
          },
          pngquant: {
            quality: '65-90',
            speed: 4
          },
          gifsicle: {
            interlaced: false,
          }
        },
      },
    ]
  },
]
複製程式碼
  1. 使用 happypack 多程式加快編譯速度

同時也需要安裝 babel 兩件套

"babel-core": "^6.26.3",
"babel-loader": "^7.1.5",
"happypack": "^5.0.0",
複製程式碼

happypack 開發生產環境都用到,配置歸入 base

// webpack.base.js
// 使用happypack
const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
// ...省略號
rules: [
  {
    test: /\.js$/,
    //把對.js 的檔案處理交給id為happyBabel 的HappyPack 的例項執行
    loader: 'happypack/loader?id=happyBabel',
    //排除node_modules 目錄下的檔案
    exclude: /node_modules/
  },
]
plugins: [
//...
new HappyPack({
      //用id來標識 happypack處理類檔案
      id: "happyBabel",
      //如何處理 用法和loader 的配置一樣
      loaders: [
        {
          loader: "babel-loader?cacheDirectory=true"
        }
      ],
      //共享程式池
      threadPool: happyThreadPool,
      //允許 HappyPack 輸出日誌
      verbose: true
    }),
 ]
複製程式碼
  1. 分離不常變化的檔案,如 node_modules 下引用的庫
// webpack.prod.js
module.exports = merge(common, {
  // ...省略號
  optimization: {
    // 分離chunks
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          name: "vendor",
          test: /[\\/]node_modules[\\/]/,
          priority: 10,
          chunks: "initial" // 只打包初始時依賴的第三方
        },
      }
    },
  },
})
複製程式碼

如此配置,則打包的 js 資料夾中會多一個 vendor.js

  1. 壓縮CSS和JS程式碼

安裝 optimize-css-assets-webpack-plugin 和 uglifyjs-webpack-plugin 外掛

// webpack.prod.js
// 壓縮CSS和JS程式碼
// ...省略號
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
module.exports = merge(common, {
  // ...省略號
  optimization: {
    // ...省略號
    minimizer: [
      // 壓縮JS
      new UglifyJsPlugin({
        uglifyOptions: {
          compress: {
            warnings: false, // 去除警告
            drop_debugger: true, // 去除debugger
            drop_console: true // 去除console.log
          },
        },
        cache: true, // 開啟快取
        parallel: true, // 平行壓縮
        sourceMap: false // set to true if you want JS source maps
      }),
      // 壓縮css
      new OptimizeCSSAssetsPlugin({})
    ]
  },
})
複製程式碼

最後,再擴充一個 hash, chunkhash, contenthash 的區別

  • hash是跟整個專案的構建相關,只要專案裡有檔案更改,整個專案構建的hash值都會更改,並且全部檔案都共用相同的hash值

  • chunkhash和hash不一樣,它根據不同的入口檔案(Entry)進行依賴檔案解析、構建對應的chunk,生成對應的雜湊值。

  • contenthash 更細緻地根據內容更改,生成對應的雜湊值。解決chunkhash 檔案中引入的檔名因 chunkhash 變動而變動的問題

專案原始碼

相關文章