Webpack構建多頁應用心得體會

Webpack構建的基於zepto的多頁應用腳手架,本文聊聊本次項目中Webpack構建多頁應用的一些心得體會。javascript

1.前言

因爲公司舊版的腳手架是基於Gulp構建的zepto多頁應用(有興趣能夠看看web-mobile-cli),有着很多的痛點。例如:css

  1. 須要兼容低版本瀏覽器,只能採用promise,不能使用awaitgenerator等。(由於babel-runtime須要模塊化);
  2. 瀏覽器緩存不友好(只能全緩存而不是使用資源文件的後綴哈希值來達到局部緩存的效果);
  3. 項目的結構不友好(能夠更好的結構化);
  4. 開發環境下的構建速度(內存);
  5. Gulp插件相對Webpack少且久遠,維護成本高等等。

此次升級有幾個地方須要注意和改進:html

  1. 項目舊代碼儘可能作到無縫轉移;
  2. 資源文件的緩存;
  3. 組件式的組織目錄結構。

Github倉庫:前端

  1. Gulp構建的舊版多頁應用web-mobile-cli
  2. Webpack構建的多頁應用web-mobile-webpack-cli

2.多頁

Webpack的多頁應用經過多入口entry和多實例html-webpack-plugin配合來構建,html-webpack-pluginchunk屬性傳入對應entrykey就能夠作到關聯,例如:java

module.exports = {
  entry: {
    pageOne: './src/pageOne/index.js',
    pageTwo: './src/pageTwo/index.js',
    pageThree: './src/pageThree/index.js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: `pageOne.html`,
      template: `./src/pageOne.html`,
      chunks: ['pageOne']
    }),
    new HtmlWebpackPlugin({
      filename: `pageTwo.html`,
      template: `./src/pageTwo.html`,
      chunks: ['pageTwo']
    }),
    new HtmlWebpackPlugin({
      filename: `pageTwo.html`,
      template: `./src/pageTwo.html`,
      chunks: ['pageTwo']
    })
  ]
}
複製代碼

那麼問題來了,開發新的頁面每次都得添加豈不是很麻煩。這裏推薦神器glob根據正則規則匹配。node

const glob = require('glob')

module.exports = {
  entry: glob.sync('./src/js/*.js').reduce((pre, filepath) => {
    const tempList = filepath.split('src/')[1].split(/js\//)
    const filename = `${tempList[0]}${tempList[1].replace(/\.js/g, '')}`
    
    return Object.assign(pre, {[filename]: filepath})
  }, {}),
  plugins: [
    ...glob.sync('./src/html/*.ejs').map((filepath, i) => {
      const tempList = filepath.split('src/')[1].split(/html\//)
      const fileName = tempList[1].split('.')[0].split(/[\/|\/\/|\\|\\\\]/g).pop()
      const fileChunk = `${tempList[0]}${fileName}`
      
      return new HtmlWebpackPlugin({
        filename: `${fileChunk}.html`,
        template: filepath,
        chunks: [fileChunk]
      })
    })
  ]
}
複製代碼

3.模板

項目沒有直接使用html,而是使用了ejs做爲模板,這裏有至少兩個好處:webpack

  1. 把公共的代碼抽離出來;
  2. 傳入公共的變量。
// header.ejs
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title><%= title %></title>
</head>

// index.ejs
<!DOCTYPE html>
<html lang="en">
<% include ./header.ejs %>
<body>
  <!-- page -->
</body>
<script src="<%= publicPath %>lib/zepto.js"></script>
</html>
複製代碼

<% include ./header.ejs %>就是引用了header.ejs文件,<%= title %><%= publicPath %>是我在配置文件定義的兩個變量,publicPath是爲了統一cdn緩存服務器的域名,很是有用。git

4.墊片

項目中使用了zepto,因此須要墊片,所謂墊片就是shim 預置依賴,即全局依賴。github

webpack compiler 可以識別遵循 ES2015 模塊語法、CommonJS 或 AMD 規範編寫的模塊。然而,一些 third party(第三方庫) 可能會引用一些全局依賴(例如 jQuery 中的 $)。所以這些 library 也可能會建立一些須要導出的全局變量。這些 "broken modules(不符合規範的模塊)" 就是 shim(預置依賴) 發揮做用的地方。web

墊片有兩種方式:

  1. 傳統方式的墊片就是在html文件中,全部引用的js文件的最前面引用的文件(例如zepto);
  2. Webpack配置shim預置依賴

最終我選擇了Webpack配置shim預置依賴這種方式,由於:

  1. 傳統的方式須要每一個頁面都手動引入(雖然說搭配ejs能夠抽離出來成爲公共模塊,但仍是須要每一個頁面手動引入公共模塊);
  2. 傳統的方式須要多發一次請求去請求墊片;
  3. Webpack能夠把全部第三方插件的代碼都拆分打包成爲一個獨立的chunk,只需一個請求。
module.exports = {
  entry: {...},
  module: {
    rules: [
      {
        test: require.resolve('zepto'),
        use: 'imports-loader?this=>window'
      }
    ]
  },
  plugins: [
    new webpack.ProvidePlugin({$: 'zepto'})
  ]
}
複製代碼

5.拆分

通常來說Webpack的配置entry中每一個key就對應輸出一個chunk,那麼該項目中會提取這幾類chunk

  1. 頁面入口(entry)對應的chunk
  2. common:屢次引用的公共文件;
  3. vender:第三方依賴;
  4. manifestWebpack運行時(runtime)代碼,它存儲着Webpackmodulechunk的信息。
module.exports = {
  entry: {...},
  module: {...},
  plugins: [],
  optimization: {
    runtimeChunk: {
      name: 'manifest'
    },
    splitChunks: {
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          chunks: 'all',
          name: 'vendors',
          filename: 'js/vendors.[contenthash:8].js',
          priority: 2,
          reuseExistingChunk: true
        },
        common: {
          test: /\.m?js$/,
          chunks: 'all',
          name: 'common',
          filename: 'js/common.[contenthash:8].js',
          minSize: 0,
          minChunks: 2,
          priority: 1,
          reuseExistingChunk: true
        }
      }
    }
  }
}
複製代碼

這裏注意的有兩點:

  1. 優先順序:第三方插件的prioritycommon代碼的priority大;
  2. 提取common代碼:minChunks爲引用次數,我設置爲引用2次即提取爲公共代碼。minSize爲最小字節,設置爲0。

6.緩存

緩存的目的是爲了提升加載速度,Webpack在緩存方面已是老生常談的了,每一個文件賦予惟一的hash值,只有更新過的文件,hash值才改變,以達到總體項目最少文件改動。

6.1 hash值

Webpack中有三種hash值:

  1. hash:所有文件同一hash,一旦某個文件改變,所有文件的hash都將改變(同一hash不知足需求);
  2. chunkhash:根據不一樣的入口文件(Entry)進行依賴文件解析、構建對應的chunk,生成對應的哈希值(問題是css做爲模塊importJavaScript文件中的,它們的chunkhash是一致的,一旦改變js文件,即便importcss文件內容沒有改變,其chunkhash值也會一同改變,不知足需求);
  3. contexthash:只有模塊的內容變了,那麼hash值才改變(採用)。
module.exports = {
  entry: {
    pageOne: './src/pageOne/index.js',
    pageTwo: './src/pageTwo/index.js',
    pageThree: './src/pageThree/index.js'
  },
  output: {
    path: 'src',
    chunkFilename: 'j[name].[contenthash:8].js',
    filename: '[name].[contenthash:8].js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: `pageOne.html`,
      template: `./src/pageOne.html`,
      chunks: ['pageOne']
    }),
    new HtmlWebpackPlugin({
      filename: `pageTwo.html`,
      template: `./src/pageTwo.html`,
      chunks: ['pageTwo']
    }),
    new HtmlWebpackPlugin({
      filename: `pageTwo.html`,
      template: `./src/pageTwo.html`,
      chunks: ['pageTwo']
    })
  ]
}
複製代碼

6.2 module id

僅僅使用contexthash還不足夠,每當import的資源文件順序改變時,chunk依然會改變,目的沒有達成。要解決這個問題首先要理解modulechunk分別是什麼,簡單理解:

  1. module:一個import對應一個module(例如:import zepto from 'zepto'中的zepto就是一個module);
  2. chunk:根據配置文件打包出來的包,就是chunk。(例如多頁應用中每一個entrykey值對應的文件)。

由於Webpack內部維護了一個自增的id,依照順序賦予給每一個module,每當新增或者刪減致使module的順序改變時,受影響的chunkhash值也會改變。解決辦法就是使用惟一的hash值替代自增的id

module.exports = {
  entry: {...},
  module: {...},
  plugins: [],
  optimization: {
    moduleIds: 'hashed'
  }
}
複製代碼

7.優化

優化的目的是提升執行和打包的速度。

7.1 查找路徑

告訴Webpack解析模塊時應該搜索的目錄,縮小編譯範圍,減小沒必要要的編譯工做。

const {resolve} = require('path')

module.exports = {
  entry: {...},
  module: {...},
  plugins: [],
  optimization: {...},
  resolve: {
    alias: {
      '@': resolve(__dirname, '../src'),
    },
    modules: [
      resolve('src'),
      resolve('node_modules'),
    ]
  }
}
複製代碼

7.2 指定目錄

指定loaderinclude目錄,做用是縮小編譯範圍。

const {resolve} = require('path')

module.exports = {
  entry: {...},
  module: {
    rules: [
      {
        test: /\.css$/,
        include: [
          resolve("src"),
        ],
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  plugins: [],
  optimization: {...},
  resolve: {...}
}
複製代碼

7.3 babel緩存目錄

babel-loader開始緩存目錄cacheDirectory

const {resolve} = require('path')

module.exports = {
  entry: {...},
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        include: [
          resolve("src"),
        ],
        use: {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true,
            presets: ['@babel/preset-env'],
            plugins: ['@babel/plugin-transform-runtime']
          }
        }
      }
    ]
  },
  plugins: [],
  optimization: {...},
  resolve: {...}
}
複製代碼

7.4 插件TerserJSPlugin

TerserJSPlugin插件的做用是壓縮JavaScript,優化的地方是開啓緩存目錄和開啓多線程。

const {resolve} = require('path')

module.exports = {
  entry: {...},
  module: {...},
  plugins: [],
  optimization: {
    minimizer: [
      new TerserJSPlugin({
        parallel: true,
        cache: true,
      })
    ]
  },
  resolve: {...}
}
複製代碼

8.總結

經過此次學習Webpack到升級腳手架,對前端工程化有了進一步的瞭解,也感覺到了Webpack4帶來的開箱即用,挺方便的。

參考文章:
Webpack官方文檔
【實戰】webpack4 + ejs + express 帶你擼一個多頁應用項目架構
基於 webpack 的持久化緩存方案

相關文章
相關標籤/搜索