多端多頁面項目Webpack打包實踐與優化

本文由 IMWeb 團隊成員 Ciccy 首發於 IMWeb 社區網站 imweb.io。點擊閱讀原文查看 IMWeb 社區更多精彩文章。css

webpack的核心是一切皆模塊,因此它其實本質上就是個靜態模塊打包器。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖,其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個 bundle。官網顯示的這幅圖很形象地描述了這個過程:html

圖片

webpack4相比於3作了不少優化,最大的改變就是支持了零配置打包,再也不強制要求必須進行繁瑣的webpack配置。 webpack4 新增了一個 mode 配置項。Mode 有兩個值:development 或者是 production,默認值是 production。webpack4 針對不一樣的mode提供了不一樣的默認配置,這對於只但願配置打包出入口,不想深刻了解其餘配置的開發人員,提供了最基礎的打包優化。固然entry,output ,mode這些配置項也都有默認值,mode默認爲production。不一樣mode的區別與默認配置能夠參考https://segmentfault.com/a/1190000013712229node

那麼接下來咱們來咱們從零開始一步步完成一個完整項目的配置,每部分配置除了會列出基礎配置,還會給出一些額外須要注意的事項,也是我在項目中的踩坑總結。webpack

先貼一下項目目錄結構:web

- src- common 公用代碼庫- pages  - [活動名稱]\_[h5|pc]    - index.js    - index.html

1、多頁面入口配置

首先咱們看看項目的打包入口如何配置: webpack打包入口支持但入口和多入口,但入口文件只限於js文件(聽說webpack5在考慮增長HTML文件和CSS文件做爲入口)。json

多入口時,給entry傳入對象便可,以下所示, 其中對象的key值則是入口的name:segmentfault

const config = {  entry: {    pageOne: './src/pageOne/index.js',    pageTwo: './src/pageTwo/index.js',    pageThree: './src/pageThree/index.js'  }};

顯然,咱們的項目頁面數量是未知的,將全部頁面都枚舉在配置裏顯然是不合理的,因此能夠定義 getEntry()方法來遍歷指定文件夾獲取入口。windows

 
 
  1. const webpack = require("webpack");api

  2. const glob = require("glob");promise


  3. function getEntry() {

  4.  const entry = {};

  5.  //讀取src目錄全部page入口

  6.  glob.sync('./src/pages/*/*/index.js')

  7.      .forEach(function (filePath) {

  8.          var name = filePath.match(/\/pages\/(.+)\/index.js/);

  9.          name = name[1];

  10.          entry[name] = filePath;

  11.      });

  12.  return entry;

  13. };


  14. module.exports = {

  15.  mode: 'development',

  16.  // 多入口

  17.  entry: getEntry(),

  18. }

2、打包輸出配置

不管是單入口仍是多入口,都只能指定一個輸出配置。咱們看看項目的 output配置

output: {  publicPath: CDN.js,  filename: '[name].[chunkhash].js',  chunkFilename: '[name]_[chunkhash].min.js',  path: distDir,},
  • filename: 輸出文件的文件

  • path: 輸出文件的絕對路徑

  • chunkFilename:非入口打包出的文件名稱

  • publicPath: 文件中靜態資源的引用路徑

一般,dev環境時,不用配置publicPath,此時靜態資源的引用路徑相對於HTML頁面。而生產環境時,把publicPath的值設爲CDN的目錄路徑就能夠了。 這裏配置有幾點須要注意的:

一、動態publicPath

這裏說了是多端多頁面項目,多端只的就是PC和H5兩端,那麼這就意味着各端的CDN資源路徑是不同的,因此publicPath值也應該不同。如何動態設置publicPath呢?

webpack 提供了 __webpack_public_path__來動態設置publicPath,咱們在入口文件的最頂部進行定義便可,以下所示 index.js

__webpack_public_path__ = myRuntimePublicPath; // 必定要寫在最頂部

二、hash值的區別

hash:以項目爲維度生成的hash值,項目所有文件都共用一個hash值 chunkhash: 以chunk爲維度生成的hash值,不一樣入口生成不一樣的chunkhash值 contenthash: 根據資源內容生成的hash值 通常是用chunkhash,contenthash也有使用場合,好比在mini-css-extract-plugin插件配置使用,後面會詳細講到。

3、loader配置

配置好了輸入輸出後,咱們就須要來配置對模塊內容如何進行處理。webpack 只能理解 JavaScript 和 JSON 文件。loader 讓 webpack 可以去處理其餘類型的文件,並將它們轉換爲有效模塊。

一、js 模塊

須要引入babel的話,咱們就須要使用babel-loader

  • js文件須要使用babel的話,引入 babel-loader

{  test: /\.js$/,  loader: 'babel-loader',  include: [path.resolve(rootDir, 'src')],},

使用babel時須要注意,Babel默認只轉換新的JavaScript句法(syntax),而不轉換新的API,好比Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局對象,以及一些定義在全局對象上的方法(好比Object.assign)都不會轉碼,若是要使用須要引入polyfill。

引入polyfill 的方式有不少種,這裏推薦babel transformtime+ runtime, transform-time的做用是將遇到須要轉化的語法時引入polyfill,而 run-time則是提供polyfill, 這樣就能夠作到按需引入,而不是全部的都打包進去。因此babel的配置以下:

{  "presets": [    [      "env",      {        "browsers": ["last 5 versions", "> 5%", "Android > 4.3"]      }    ],    "stage-2"  ],  "plugins": [    "transform-runtime"  ]}

二、css 模塊

對於css模塊,經常使用的loader有style-loader和css-loader。 css loader用來處理js文件中引入的css模塊(處理@import和url()), style-loader是將 css-loader打包好的css代碼以 <style>標籤的形式插入到html文件中。

這個項目用到了sass和post-css,因此這裏還引入了sass-loader和postcss-loader。由於webpack對於loader的調用是從右往左的,因此配置以下:

{  // 增長對 SCSS 文件的支持  test: /\.scss|\.css/,  // SCSS 文件的處理順序爲先 sass-loader 再 css-loader 再 style-loader  use: [    'style-loader',    {      loader: 'css-loader',      // 給 css-loader 傳入配置項      options: {        importLoaders: 2,      },    },    'postcss-loader',    {      loader: 'sass-loader',    },  ],},

若是你也使用了sass-loader,有個問題可能須要注意。當你的index.scss裏@import了其餘scss文件好比a.scss時,若是a.scss裏使用了url(),且裏面的路徑是相對路徑,那麼在sass-loader 處理事後給css-loader處理時就會報錯,找不到url()裏指定的資源。這是爲何呢?

實際上,當sass-loader處理時,會將index.scss裏@import的A.scss合併進來,最後只輸出index.scss。但A.scss裏的url()原本是以A.scss寫的相對路徑,這樣合併又不對url()作處理的話,就致使了合併後沒法定位到url()裏的資源。對於這個問題,有兩種解決辦法:

  • 1)使用 resolve-url-loader,將 resolve-url-loader設置於 loader 鏈中的 sass-loader 以前,就能夠重寫 url。可是這個辦法有個問題,那就是 resolve-url-loader不識別scss文件的行內註釋語法,即 // 註釋,這個問題使得接入一些已存在的公共樣式庫時會存在問題,目前還在研究是否有其餘loader能夠解決,你們有較好的解決辦法也能夠一塊兒討論。

  • 2)將資源路徑改成變量來統一管理

  • 3)經過alias設置路徑別名,從而便捷使用絕對路徑。注意在scss文件中使用alias裏定義的路徑別名時,須要帶上~前綴,不然打包時仍會被識別爲普通路徑

三、圖片、字體等資源

對於圖片等其餘資源,咱們通常使用file-loader進行處理,它實現的功能很簡單:

  • 將要加載的文件複製到指定目錄

  • 生成請求文件資源URL 具體配置以下:

{  test: /\.(gif|png|jpe?g|eot|woff|ttf|pdf)$/,  loader: 'file-loader',},

四、import AMD 模塊

儘管webpack既支持commonjs規範也支持AMD規範。可是咱們如何經過import 的方式引入AMD 模塊或者其餘不支持模塊化的庫呢?

咱們項目裏使用到了zepto,這裏就以zepto爲例,在import zepto時會報錯

Uncaught TypeError: Cannot read property 'createElement' of undefined

這就是由於zepto只使用了AMD 規範導出模塊。解決全部這類問題其實很簡單,只須要使用 script-loaderexports-loader便可:

{    test: require.resolve('zepto'),    use: ['exports-loader?window.Zepto','script-loader']}
  • script-loader 用 eval 的方法將 zepto 在引入的時候執行了一遍,此時 zepto 庫已存在於 window.Zepto

  • exports-loader 將傳入的 window.Zepto 以 module.exports = window.Zepto 的形式向外暴露接口,使這個模塊符合 CommonJS 規範,支持 import 這樣咱們就能夠直接 import$from'zepto'了,其餘AMD 模塊或者其餘不支持模塊化的庫也相似。

4、plugin 配置

插件機制是webpack的核心之一,插件(Plugins)是用來拓展webpack功能的,它們會在整個構建過程當中生效,執行相關的任務。咱們通常使用插件來完善咱們的構建流程,webpack有許多插件可用,這裏只挑兩個必備插件來詳細說明

一、html-webpack-plugin

前面有說過,目前webpack的打包入口只支持JS文件,因此它打包輸出的也是JS文件,那麼如何把這個JS文件引入咱們的html中去呢,手動引入沒法監測到hash值的變化,確定是不OK的。所以咱們就用到了 html-webpack-plugin這個插件,它會將打包好的文件自動引入到指定的html中去,並將html文件輸出在指定位置。

html-webpack-plugin使用時,一個實例操做只能一個html,因此對於多頁面項目,咱們須要創造多個實例,結合前面的getEntry方法,咱們能夠在遍歷獲得entry的時候進行實例化,獲得htmlPluginArray

 
 
  1. const htmlPluginArray= [];


  2. function getEntry() {

  3.   const entry = {};

  4.   glob.sync('./src/pages/*/*/index.js')

  5.       .forEach(function (filePath) {

  6.           var name = filePath.match(/\/pages\/(.+)\/index.js/);

  7.           name = name[1];

  8.           entry[name] = filePath;


  9.            // 實例化插件

  10. +         htmlPluginArray.push(newHtmlWebpackPlugin({

  11. +         filename: './' + name + '/index.html',

  12. +         template: './src/pages/' + name + '/index.html',

  13. +          }))


  14. });  

  15.   return entry;

  16. };


  17. // 配置plugin,此處省略其餘配置代碼

  18.  plugins: [

  19.        htmlPluginArray

  20.  ],

二、mini-css-extract-plugin

前面使用css loader 和 style-loader對css文件進行處理後,css文件被做爲模塊也打包在了js文件中。實際生產環境,咱們固然是但願js文件和css文件分離的,因此這裏就可使用 mini-css-extract-plugin。 具體配置以下:

  module: {    rules: [      {        // 增長對 SCSS 文件的支持        test: /\.scss|\.css/,        // SCSS 文件的處理順序爲先 sass-loader 再 css-loader 再 style-loader        use: [          {+            loader: MiniCssExtractPlugin.loader,+            options: {+             publicPath: CDN.css,            },          },          {            loader: 'css-loader',            // 給 css-loader 傳入配置項            options: {              importLoaders: 2,            },          },          'postcss-loader',          {            loader: 'sass-loader',          },        ],      }    ],  },  plugins: [    new MiniCssExtractPlugin({      filename: '[name].[contenthash].css',      chunkFilename: '[name].[contenthash].css',    }),  ],

這裏之因此設置爲 contenthash,是用來解決抽離css文件後,js文件變化致使的css文件hash值變化的問題

5、其餘配置

一、resolve

resolve配置規定了webpack如何尋找各個依賴模塊。

前面講到的alias就是在這裏配置。在資源引用時,若是資源引用路徑太深,又比較經常使用,咱們則能夠定義路徑別名,例如:

alias: {  h5: path.resolve(__dirname, 'src/common/h5/'),  pc: path.resolve(__dirname, 'src/common/pc/'),}

咱們就能夠直接在代碼中這樣引用了:

import Utility from 'h5/util';

二、webpack dev server

webpack-dev-server 是開發時的必備利器,它能夠在本地起一個簡單的 web 服務器,當文件發生變化時,可以實時從新加載。 webpack-dev-server的配置也很簡單:

devServer: {    publicPath: '/act/',    port: 8888,    hot: true,},

啓動webpack-dev-server後,在目標文件夾中是看不到編譯後的文件的,實時編譯後的文件都保存到了內存當中

1) HMR

hot設置爲true是啓用 webpack 的 模塊熱替換(HMR)功能,但這裏注意必需要添加插件 webpack.HotModuleReplacementPlugin 才能徹底啓用 HMR

2) publicPath

publicPath路徑下的打包文件能夠在瀏覽器中訪問,能夠這麼理解,webpack-dev-server打包的內容是放在內存中的,這些打包後的資源對外的的根目錄就是publicPath。

默認 devServer.publicPath 是 '/',因此你的包(bundle)能夠經過 http://localhost:8888/bundle.js 訪問。當咱們要設置具體路徑時記得要以 /開頭,如上面配置所示,設置了 publicPath:'/act/'後bundle的訪問路徑則變成了: http://localhost:8888/act/bundle.js注意:當這裏的publicPath和output的publicPath同時設置時,這裏的優先級更高

三、配置分離

一般,咱們本地開發環境和生產環境會採用不一樣的配置文件,發佈上線時,咱們會對資源進行壓縮、合併等優化,但在本地開發時,爲了提升構建速度,方便調試代碼,咱們則會省去這些優化配置,與此同時,咱們更加關注模塊熱更新、localhost server等等。因此通常會爲每一個環境編寫彼此獨立的 webpack 配置,這裏項目的webpack配置文件以下,其中webpack.common.js是用來放dev和dist裏的公共配置:

這裏會用到 webpack-merge工具進行配置的合併。 好比 webpack.common.js內容以下:

module.exports = {  module: {    rules: []  }};

webpack.dev.js的則可使用webpack-merge合併配置:

const merge = require('webpack-merge');const common = require('./webpack.common.js');module.exports = merge(common, {   devtool: 'inline-source-map',   devServer: {   // dev 配置 }});

因此咱們能夠在package.json添加咱們的webpack啓動命令以下:

"scripts": {  "dist": "cross-env NODE_ENV=production webpack --config webpack.dist.js",  "dev": "webpack-dev-server --config webpack.dev.js",},

其中, cross-env NODE_ENV=production是用來設置node環境變量,設置環境變量的目的是由於許多庫自身會判斷當前環境,並在生產環境下作一些優化處理,而用cross-env來設置是爲了兼容windows系統。

6、優化

到這裏,咱們項目已經能起來了,可是做爲一名合格的程序猿,咱們固然要探索更優實踐。webpack有哪些經常使用的優化措施呢?

一、按需加載

webpack 提供了兩種動態加載的語法。第一種,也是推薦選擇的方式是,使用符合 ECMAScript 提案 的 import() 語法 來實現動態導入。第二種,則是 webpack 的遺留功能,使用 webpack 特定的 require.ensure。

import() 會返回一個 promise,在代碼中全部被import()的模塊,都將打成一個單獨的包,在瀏覽器運行到這一行代碼時,就會自動請求這個資源,實現動態加載。* 使用import()時應該注意如下幾點: *

  • 1)import()時能夠經過註釋語法import(/chunkName/'qqapi').then()來定義異步加載模塊打包出來的chunkName,不然會默認以id做爲chunkName

  • 2) 當bundle中已經以同步方式引入模塊後,import()將不會再被webpack單獨打包出js文件,能夠認爲是按需加載無效了

二、抽離公共模塊

1)通常項目

爲了合理利用瀏覽器緩存,通常會將不常變更的第三方庫以及公共代碼和業務代碼分開打包

因此通常項目的打包策略爲:

  • 第三方庫打包出vendor(基本不變)

  • 引用兩次以上的模塊打包出common (變化較少)

  • 業務代碼 (常變)

對於分包方式,webpack 4 移除 CommonsChunkPlugin,取而代之的是optimization.splitChunks  讓咱們看看這裏怎麼配置:

splitChunks: {    cacheGroups: {      vendor: {        test: /[\\/]node_modules[\\/]/        name: 'vendor',        chunks: 'initial',        priority: 2,        minChunks: 2      },      common: {        test: /.js$/,        name: 'common',        chunks: 'initial',        priority: 1,        minChunks: 2      }}}

注意抽離出來的代碼要在HTML文件裏引入

2)多端項目

因爲項目包含兩端代碼,H5\PC部分依賴是獨立的,單純的從項目層面進行公共模塊的抽離是不行的。

因此這裏得詳細設置公共庫和代碼的匹配規則。好比咱們項目PC用的JQ,H5用的zepto,就能夠配置

optimization: {    splitChunks: {      cacheGroups: {        h5common: {          test: /zepto/,          name: 'h5common',          chunks: 'initial',          priority: 1,          minChunks: 1,        },      },    }, },

三、優化loader配置

配置loader時,咱們能夠經過exclude設置哪些目錄下的文件不進行處理,經過include精確指定只處理哪些目錄下的文件,以此來縮小處理範圍,加快構建速度。

module: {    rules: [        {            test: /\.js$/,            use: 'babel-loader',            exclude: /node_modules/,             include: path.resolve(__dirname, 'src')         }    ]}

四、限制路徑解析範圍

當咱們引用模塊時,若是出現import ‘zepto’這樣的依賴引入方式,webpack會默認從當前目錄往上逐層查找是否有 node_modules,而後在 node_modules下查找是否存在指定依賴。

爲了減小搜索範圍,咱們能夠經過設置 resolve.modules來告訴 webpack 解析這類依賴時應該搜索的目錄

resolve: {  modules: [path.resolve(rootDir, 'node_modules')],},

總結

這篇文章以多端多頁面項目爲例,深刻講解了如何初始化項目webpack配置,這些實踐不只適用於這個項目,對於多頁面項目和普通項目也一樣適用。

相關文章
相關標籤/搜索