Webpack 4手工搭建重點分析

前言

這是一篇關於webpack 4手工搭建重點問題的分析,webpack 3相關能夠戳這裏: https://github.com/Eleven90/webpack-pages-V3,這裏並不試圖從零手把手去堆代碼,而是對其中的重點問題作稍微深刻一點的解讀。某些細節這裏若是沒有說起,項目代碼裏多半已解決。

項目地址:https://github.com/Eleven90/webpack-template

Babel 8

這是最新的 babel 配置,和網上的諸多教程可能有不一樣,能夠自行測試驗證有效性。
  1. 基礎依賴包css

    npm i babel-loader@8 @babel/core -D
    從 babel7 開始,全部的官方插件和主要模塊,都放在了 @babel 的命名空間下。從而能夠避免在 npm 倉庫中 babel 相關名稱被搶注的問題。
  2. 在 package.json 同級添加.babelrc 配置文件,先空着。html

    {
      "presets": [],  // 預設
      "plugins": []   // 插件
    }
  3. package.json 文件能夠聲明須要支持到的瀏覽器版本前端

    1. package.json 中聲明的 browserslist 能夠影響到 babel、postcss,babel 是優先讀取.babelrc 文件中@babel/preset-env 的 targets 屬性,未定義會讀取 package.json 中的 browserslist。
      爲了統一,在 package.json 中定義。
    2. package.json 中定義(推薦)node

      "browserslist": [
        "> 1%",
        "last 2 versions",
        "not ie <= 8"
      ],
      更多定義格式請查看: browserslist
    3. .babelrc 中定義(不推薦)jquery

      {
        "presets": [
          [
            "@babel/preset-env",
            {
              "targets": {
                "chrome": "58",
                "ie": "11"
              }
            }
          ]
        ]
      }
  4. Babel 默認只轉換新的 JavaScript 句法(syntax),而不轉換新的 API,好比 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局對象,以及一些定義在全局對象上的方法(好比 Object.assign)都不會轉碼。
    須要轉譯新的 API,使用@babel/preset-env@babel/plugin-transform-runtime,二選一便可。
  5. 使用@babel/preset-envwebpack

    1. 安裝依賴包:git

      npm i @babel/preset-env @babel/polyfill -D
    2. .babelrc 文件寫上配置,@babel/polyfill 不用寫入配置,會自動被調用。es6

      {
        "presets": [
          [
            "@babel/preset-env",
            {
              "useBuiltIns": "entry",
              "modules": false,
            }
          ]
        ]
      }
    3. 配置參數github

      1. modules參數,"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默認值是 auto。
        用來轉換 ES6 的模塊語法。若是使用 false,將不會對文件的模塊語法進行轉化。
        若是要使用 webpack 中的一些新特性,好比 tree shaking 和 sideEffects,就須要設置爲 false,對 ES6 的模塊文件不作轉化,由於這些特性只對 ES6 的模塊有效。
      2. useBuiltIns參數,"usage" | "entry" | false,默認值是 false。web

        • false:須要在 js 代碼第一行主動 import '@babel/polyfill',會將@babel/polyfill 整個包所有導入。
          (不推薦,能覆蓋到全部 API 的轉譯,但體積最大)
        • entry:須要在 js 代碼第一行主動 import '@babel/polyfill',會將 browserslist 環境不支持的全部墊片都導入。
          (可以覆蓋到‘hello‘.includes(‘h‘)這種句法,足夠安全且代碼體積不是特別大)
        • usage:項目裏不用主動 import,會自動將代碼裏已使用到的、且 browserslist 環境不支持的墊片導入。
          (可是檢測不到‘hello‘.includes(‘h‘)這種句法,對這類原型鏈上的句法問題不會作轉譯,書寫代碼需注意
      3. targets參數,用來配置須要支持的的環境,不只支持瀏覽器,還支持 node。若是沒有配置 targets 選項,就會讀取項目中的 browserslist 配置項。
      4. loose參數,默認值是 false,若是 preset-env 中包含的 plugin 支持 loose 的設置,那麼能夠經過這個字段來作統一的設置。
  6. 使用@babel/plugin-transform-runtime

    1. 安裝依賴包

      npm i @babel/plugin-transform-runtime -D
      1. 若是配置參數 corejs 未設置或爲 false,需安裝依賴@babel/runtime(這部分代碼會被抽離並打包到應用 js 裏,因此能夠安裝在 dependencies 裏),僅對 es6 語法轉譯,而不對新 API 轉譯。

        npm i @babel/runtime
      2. 若是配置參數 corejs 設置爲 2,需安裝依賴@babel/runtime-corejs2(同上,推薦安裝在 dependencies 裏。),對語法、新 API 都轉譯。

        npm i @babel/runtime-corejs2
      3. 推薦使用corejs:2,可是,檢測不到‘hello‘.includes(‘h‘)這種句法,因此存在必定隱患,書寫代碼時需注意。
      4. @babel/runtime@babel/runtime-corejs2這兩個庫惟一的區別是:corejs2 這個庫增長了對 core-js(用來對 ES6 各個語法 polyfill 的庫)這個庫的依賴,因此在 corejs 爲 false 的狀況下,只能作語法的轉換,並不能 polyfill 任何新 API。
    2. .babelrc 文件寫上配置

      {
         "presets": [],
         "plugins": [
           [
             "@babel/plugin-transform-runtime",
             {
               "corejs": 2 // 推薦
             }
           ]
         ]
       }
    3. 配置參數

      1. corejs,默認值是 false,只對語法進行轉換,不對新 API 進行處理;當設置爲 2 的時候,須要安裝@babel/runtime-corejs2,這時會對 api 進行處理。
      2. helpers,默認值是 true,用來開啓是否使用 helper 函數來重寫語法轉換的函數。
      3. useESModules,默認值是 false,是否對文件使用 ES 的模塊語法,使用 ES 的模塊語法能夠減小文件的大小。
  7. @babel/preset-env仍是@babel/plugin-transform-runtime? (傳送門:babel polyfill 和 runtime 淺析)

    1. @babel/preset-env + @babel/polyfill能夠轉譯語法、新 API,但存在污染全局問題;
    2. @babel/plugin-transform-runtime + @babel/runtime-corejs2,可按需導入,轉譯語法、新 API,且避免全局污染(babel7 中@babel/polyfill 是@babel/runtime-corejs2 的別名),可是檢測不到‘hello‘.includes(‘h‘)這種句法;
    3. @babel/polyfill 和@babel/runtime-corejs2 都使用了 core-js(v2)這個庫來進行 api 的處理。
      core-js(v2)這個庫有兩個核心的文件夾,分別是 library 和 modules。@babel/runtime-corejs2 使用 library 這個文件夾,@babel/polyfill 使用 modules 這個文件夾。

      1. library 使用 helper 的方式,局部實現某個 api,不會污染全局變量; 而 modules 以污染全局變量的方法來實現 api;
      2. library 和 modules 包含的文件基本相同,最大的不一樣是_export.js 這個文件:

        // core-js/modules/_exports.js
        var global = require('./_global');
        var core = require('./_core');
        var hide = require('./_hide');
        var redefine = require('./_redefine');
        var ctx = require('./_ctx');
        var PROTOTYPE = 'prototype';
        
        var $export = function (type, name, source) {
          var IS_FORCED = type & $export.F;
          var IS_GLOBAL = type & $export.G;
          var IS_STATIC = type & $export.S;
          var IS_PROTO = type & $export.P;
          var IS_BIND = type & $export.B;
          var target = IS_GLOBAL ? global : IS_STATIC ? global[name] || (global[name] = {}) : (global[name] || {})[PROTOTYPE];
          var exports = IS_GLOBAL ? core : core[name] || (core[name] = {});
          var expProto = exports[PROTOTYPE] || (exports[PROTOTYPE] = {});
          var key, own, out, exp;
          if (IS_GLOBAL) source = name;
          for (key in source) {
            // contains in native
            own = !IS_FORCED && target && target[key] !== undefined;
            // export native or passed
            out = (own ? target : source)[key];
            // bind timers to global for call from export context
            exp = IS_BIND && own ? ctx(out, global) : IS_PROTO && typeof out == 'function' ? ctx(Function.call, out) : out;
            // extend global
            if (target) redefine(target, key, out, type & $export.U);
            // export
            if (exports[key] != out) hide(exports, key, exp);
            if (IS_PROTO && expProto[key] != out) expProto[key] = out;
          }
        };
        global.core = core;
        // type bitmap
        $export.F = 1;   // forced
        $export.G = 2;   // global
        $export.S = 4;   // static
        $export.P = 8;   // proto
        $export.B = 16;  // bind
        $export.W = 32;  // wrap
        $export.U = 64;  // safe
        $export.R = 128; // real proto method for `library`
        module.exports = $export;
        // core-js/library/_exports.js
        var global = require('./_global');
        var core = require('./_core');
        var ctx = require('./_ctx');
        var hide = require('./_hide');
        var has = require('./_has');
        var PROTOTYPE = 'prototype';
        
        var $export = function (type, name, source) {
          var IS_FORCED = type & $export.F;
          var IS_GLOBAL = type & $export.G;
          var IS_STATIC = type & $export.S;
          var IS_PROTO = type & $export.P;
          var IS_BIND = type & $export.B;
          var IS_WRAP = type & $export.W;
          var exports = IS_GLOBAL ? core : core[name] || (core[name] = {});
          var expProto = exports[PROTOTYPE];
          var target = IS_GLOBAL ? global : IS_STATIC ? global[name] : (global[name] || {})[PROTOTYPE];
          var key, own, out;
          if (IS_GLOBAL) source = name;
          for (key in source) {
            // contains in native
            own = !IS_FORCED && target && target[key] !== undefined;
            if (own && has(exports, key)) continue;
            // export native or passed
            out = own ? target[key] : source[key];
            // prevent global pollution for namespaces
            exports[key] = IS_GLOBAL && typeof target[key] != 'function' ? source[key]
            // bind timers to global for call from export context
            : IS_BIND && own ? ctx(out, global)
            // wrap global constructors for prevent change them in library
            : IS_WRAP && target[key] == out ? (function (C) {
              var F = function (a, b, c) {
                if (this instanceof C) {
                  switch (arguments.length) {
                    case 0: return new C();
                    case 1: return new C(a);
                    case 2: return new C(a, b);
                  } return new C(a, b, c);
                } return C.apply(this, arguments);
              };
              F[PROTOTYPE] = C[PROTOTYPE];
              return F;
            // make static versions for prototype methods
            })(out) : IS_PROTO && typeof out == 'function' ? ctx(Function.call, out) : out;
            // export proto methods to core.%CONSTRUCTOR%.methods.%NAME%
            if (IS_PROTO) {
              (exports.virtual || (exports.virtual = {}))[key] = out;
              // export proto methods to core.%CONSTRUCTOR%.prototype.%NAME%
              if (type & $export.R && expProto && !expProto[key]) hide(expProto, key, out);
            }
          }
        };
        // type bitmap
        $export.F = 1;   // forced
        $export.G = 2;   // global
        $export.S = 4;   // static
        $export.P = 8;   // proto
        $export.B = 16;  // bind
        $export.W = 32;  // wrap
        $export.U = 64;  // safe
        $export.R = 128; // real proto method for `library`
        module.exports = $export;
      3. 能夠看出,library下的這個$export方法,會實現一個wrapper函數,防止污染全局變量。
      4. 例如對Promise的轉譯,@babel/polyfill和@babel/runtime-corejs2的轉譯方式差別以下:

        var p = new Promise();
        
        // @babel/polyfill
        require("core-js/modules/es6.promise");
        var p = new Promise();
        
        // @babel/runtime-corejs2
        var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
        var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));
        var a = new _promise.default();
      5. 從上面這個例子能夠看出,對於Promise這個api,@babel/polyfill引用了core-js/modules中的es6.promise.js文件,由於是對全局變量進行處理,因此賦值語句不用作處理;@babel/runtime-corejs2會生成一個局部變量_promise,而後把Promise都替換成_promise,這樣就不會污染全局變量了。
    4. 綜合上面的分析,得出結論:

      1. 若是是本身的應用: @babel/preset-env + @babel/polyfill

        1. useBuiltIns設置爲entry比較不錯。
          在js代碼第一行import '@babel/polyfill',或在webpack的入口entry中寫入模塊@babel/polyfill,會將browserslist環境不支持的全部墊片都導入;
          可以覆蓋到‘hello‘.includes(‘h‘)這種句法,足夠安全且代碼體積不是特別大,推薦使用!
        2. useBuiltIns設置爲usage
          項目裏不用主動import,會自動將代碼裏已使用到的、且browserslist環境不支持的墊片導入;
          相對安全且打包的js體積不大,可是,一般咱們轉譯都會排除node_modules/目錄,若是使用到的第三方包有個別未作好ES6轉譯,有遇到bug的可能性,而且檢測不到‘hello‘.includes(‘h‘)這種句法。
          代碼書寫規範,且信任第三方包的時候,可使用!
        3. useBuiltIns設置爲false比較不錯。
          在js代碼第一行import '@babel/polyfill',或在webpack的入口entry中寫入模塊@babel/polyfill,會將@babel/polyfill整個包所有導入;
          最安全,但打包體積會大一些,通常不選用。

          須要安裝的所有依賴:

          npm i babel-loader@8 @babel/core @babel/preset-env -D
          npm i @babel/polyfill

          .babelrc配置文件

          {
            "presets": [
              [
                "@babel/preset-env",
                {
                  "modules": false, // 推薦
                  "useBuiltIns": "entry", // 推薦
                }
              ]
            ],
            "plugins": []
          }
      2. 若是是開發第三方類庫: @babel/plugin-transform-runtime + @babel/runtime-corejs2
        (或者,不作轉碼處理,提醒使用者本身作好兼容處理也能夠。)

        須要安裝的所有依賴:

        npm i babel-loader@8 @babel/core @babel/plugin-transform-runtime -D
         npm i @babel/runtime-corejs2

        .babelrc配置文件

        {
               "presets": [],
               "plugins": [
                 [
                   "@babel/plugin-transform-runtime",
                   {
                     "corejs": 2 // 推薦
                   }
                 ]
               ]
             }
  8. Babel 官方認爲,把不穩定的 stage0-3 做爲一種預設是不太合理的,@babel/preset-env@babel/polyfill等只支持到stage-4級別,所以 babel 新版本廢棄了 stage 預設,轉而讓用戶本身選擇使用哪一個 proposal 特性的插件,這將帶來更多的明確性(用戶無須理解 stage,本身選的插件,本身便能明確的知道代碼中可使用哪一個特性)。
    全部建議特性的插件,都改變了命名規範,即相似 @babel/plugin-proposal-function-bind 這樣的命名方式來代表這是個 proposal 階段特性。
    因此,處於建議階段的特性,基本都已從@babel/preset-env@babel/polyfill等包中被移除,須要本身去另外安裝對應的 preset、plugin,(通常你能找到的名稱裏有 proposal 字樣的包,須要本身在@babel/preset-env@babel/plugin-transform-runtime之外作配置)。
    各個級別當前能夠選用的 proposal 插件大概以下(傳送門):

    {
        "plugins": [
        // Stage 0
        "@babel/plugin-proposal-function-bind",
    
        // Stage 1
        "@babel/plugin-proposal-export-default-from",
        "@babel/plugin-proposal-logical-assignment-operators",
        ["@babel/plugin-proposal-optional-chaining", { "loose": false }],
        ["@babel/plugin-proposal-pipeline-operator", { "proposal": "minimal" }],
        ["@babel/plugin-proposal-nullish-coalescing-operator", { "loose": false }],
        "@babel/plugin-proposal-do-expressions",
    
        // Stage 2
        ["@babel/plugin-proposal-decorators", { "legacy": true }],
        "@babel/plugin-proposal-function-sent",
        "@babel/plugin-proposal-export-namespace-from",
        "@babel/plugin-proposal-numeric-separator",
        "@babel/plugin-proposal-throw-expressions",
    
        // Stage 3
        "@babel/plugin-syntax-dynamic-import",
        "@babel/plugin-syntax-import-meta",
        ["@babel/plugin-proposal-class-properties", { "loose": false }],
        "@babel/plugin-proposal-json-strings"
      ]
    }
  9. 配置裝飾器語法支持

    1. 安裝依賴

      npm i @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties -D
    2. .babelrc 增長配置

      {
        "presets": [],
        "plugins": [
          [
            "@babel/plugin-proposal-decorators",  // @babel/plugin-proposal-decorators須要在@babel/plugin-proposal-class-properties以前
            {
              "legacy": true // 推薦
            }
          ],
          [
            "@babel/plugin-proposal-class-properties",
            {
              "loose": true // babel編譯時,對class的屬性採用賦值表達式,而不是Object.defineProperty(更簡潔)
            }
          ]
        ]
      }
  10. 配置 import 動態導入支持

    1. 安裝依賴

      npm i @babel/plugin-syntax-dynamic-import -D
    2. .babelrc 文件增長配置

      {
        "presets": [],
        "plugins": [
          "@babel/plugin-syntax-dynamic-import",
        ]
      }

自動掃描 webpack 入口文件和 html 模版文件

正常若是有多個入口,須要在 entry 中,以對象形式將全部入口都配置一遍,html 模版目錄也須要 new 不少個 HtmlWebpackPlugin 來配置對應的頁面模版,是否能夠自動掃描? 不管多少個入口,只管新建,而不用管理入口配置?能夠的!
  1. 安裝 node 模塊 glob ( 掃描文件就靠它了 ).

    npm i glob -D
    const glob = require('glob')
  2. 自動掃描獲取入口文件、html 模版(統一放在 utils.js 文件裏)

    /**
     * 獲取文件
     * @param {String} filesPath 文件目錄
     * @returns {Object} 文件集合(文件名: 文件路徑)
     */
    const getFiles = filesPath => {
      let files = glob.sync(filesPath)
      let obj = {}
      let filePath, basename, extname
    
      for (let i = 0; i < files.length; i++) {
        filePath = files[i]
        extname = path.extname(filePath) // 擴展名 eg: .html
        basename = path.basename(filePath, extname) // 文件名 eg: index
        // eg: { index: '/src/views/index/index.js' }
        obj[basename] = path.resolve(appDirectory, filePath)
      }
      return obj
    }
    
    /**
     * 打包入口
     *  1.容許文件夾層級嵌套;
     *  2.入口js的名稱不容許重名;
     */
    const entries = getFiles('src/views/**/*.js')
    
    /**
     * 頁面的模版
     *  1.容許文件夾層級嵌套;
     *  2.html的名稱不容許重名;
     */
    const templates = getFiles('src/views/**/*.html')
    
    /**
     * 獲取entry入口,爲了處理在某些時候,entry入口會加 polyfill等:
     *  1.容許文件夾層級嵌套;
     *  2.入口的名稱不容許重名;
     *
     * @returns {Object} entry 入口列表(對象形式)
     */
    const getEntries = () => {
      let entry = {}
    
      for (let name in entries) {
        entry[name] = entries[name]
      }
      return entry
    }
  3. webpack 打包入口

    module.exports = {
      entry: utils.getEntries(),
    }
  4. html 模版自動引入打包資源(區分 dev 和 prod 環境,配置不一樣,一樣抽離到 utils.js 文件更好一些)

    /**
     * 生成webpack.config.dev.js的plugins下new HtmlWebpackPlugin()配置
     * @returns {Array} new HtmlWebpackPlugin()列表
     */
    const getHtmlWebpackPluginsDev = () => {
      let htmlWebpackPlugins = []
      let setting = null
    
      for (let name in templates) {
        setting = {
          filename: `${name}.html`,
          template: templates[name],
          inject: false, // js插入的位置,true/'head'/'body'/false
        }
    
        // (僅)有入口的模版自動引入資源
        if (name in getEntries()) {
          setting.chunks = [name]
          setting.inject = true
        }
        htmlWebpackPlugins.push(new HtmlWebpackPlugin(setting))
        setting = null
      }
    
      return htmlWebpackPlugins
    }
    
    /**
     * 生成webpack.config.prod.js的plugins下new HtmlWebpackPlugin()配置
     * @returns {Array} new HtmlWebpackPlugin()列表
     */
    const getHtmlWebpackPluginsProd = () => {
      let htmlWebpackPlugins = []
      let setting = null
    
      for (let name in templates) {
        setting = {
          filename: `${name}.html`,
          template: templates[name],
          minify: {
            removeComments: true,
            collapseWhitespace: true,
            removeRedundantAttributes: true,
            useShortDoctype: true,
            removeEmptyAttributes: true,
            removeStyleLinkTypeAttributes: true,
            keepClosingSlash: true,
            minifyJS: true,
            minifyCSS: true,
            minifyURLs: true,
          },
          inject: false, // js插入的位置,true/'head'/'body'/false
        }
    
        // (僅)有入口的模版自動引入資源
        if (name in getEntries()) {
          setting.chunks = ['manifest', 'vendor', 'common', name]
          setting.inject = true
        }
        htmlWebpackPlugins.push(new HtmlWebpackPlugin(setting))
        setting = null
      }
    
      return htmlWebpackPlugins
    }

CSS 樣式的處理(less 預編譯和 postcss 工具)

  1. 須要安裝的依賴包

    npm i less less-loader css-loader style-loader postcss-loader postcss-preset-env postcss-import cssnano postcss-safe-parser mini-css-extract-plugin -D
    過去版本的autoprefixer、postcss-cssnext已內置在postcss-preset-env內。
  2. 配置

    默認會將 css 一塊兒打包到 js 裏,藉助 mini-css-extract-plugin 將 css 分離出來並自動在生成的 html 中 link 引入(過去版本中的 extract-text-webpack-plugin 已不推薦使用)。

    const MiniCssExtractPlugin = require('mini-css-extract-plugin')
    {
         test: /\.(less|css)$/,
         use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'],
     }
    
     // 在啓用dev-server時,mini-css-extract-plugin插件不能使用contenthash給文件命名 => 因此本地起dev-server服務調試時,使用style-loader
     // USE_HMR是自定義的環境變量,意思是是否使用了熱替換中間件
     const styleLoader = process.env.USE_HMR ? 'style-loader' : MiniCssExtractPlugin.loader
    
     // 經過其餘合適的方式判斷是否爲本地調試環境也同樣,自由選擇。
     const styleLoader = process.env.BUILD_ENV === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader
    
     {
       test: /\.(less|css)$/,
       use: [styleLoader, 'css-loader', 'postcss-loader', 'less-loader'],
     },
    // 單獨使用link標籤加載css並設置路徑,相對於output配置中的publickPath
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:7].css', // 注意這裏使用的是contenthash,不然任意的js改動,打包時都會致使css的文件名也跟着變更。
      chunkFilename: 'static/css/[name].[contenthash:7].css',
    })
  3. PostCSS 自己不會對你的 CSS 作任何事情, 你須要安裝一些 plugins 才能開始工做.
    參考文檔:postcss GitHub 文檔

    在 package.json 同級目錄新建 postcss.config.js 文件:

    module.exports = {
      // parser: 'sugarss', // 是一個以縮進爲基礎的語法,相似於 Sass 和 Stylus,https://github.com/postcss/sugarss
      plugins: {
        'postcss-import': {},
        'postcss-preset-env': {},
        'cssnano': {},
        'postcss-flexbugs-fixes': {},
      }
    }

    經常使用的插件:

    • cssnano —— 會壓縮你的 CSS 文件來確保在開發環境中下載量儘量的小
    • postcss-pxtorem —— px 單位自動轉換 rem
    • postcss-assets —— 插件用來處理圖片和 SVG, 相似 url-load
    • postcss-sprites —— 將掃描你 CSS 中使用的全部圖像,自動生成優化的 Sprites 圖像和 CSS Sprites 代碼
    • postcss-font-magician —— 使用自定義字體時, 自動搞定@font-face 聲明
    less 是預處理,而 PostCSS 是後處理,基本支持 less 等預處理器的功能,自動添加瀏覽器廠商前綴向前兼容,容許書寫下一代 css 語法 ,能夠在編譯時去除冗餘的 css 代碼,PostCSS 聲稱比預處理器快 3-30 倍. 由於 PostCSS,可能咱們要放棄 less/sass/stylus 了

圖片、字體、多媒體等資源的處理

  1. css 中引入的圖片( 或其它資源 ) ==> url-loader
    配置了 url-loader 之後,webpack 編譯時能夠自動將小圖轉成 base64 編碼,將大圖改寫 url 並將文件生成到指定目錄下 ( file-loader 能夠完成文件生成,可是不能小圖轉 base64,因此統一用 url-loader,但 url-loader 在處理大圖的時候是自動去調用 file-loader,因此你仍然須要 install file-loader )。

    // 處理圖片(file-loader來處理也能夠,url-loader更適合圖片)
    {
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      loader: 'url-loader',
      options: {
        limit: 10000,
        name: 'static/assets/images/[name].[hash:7].[ext]',
      },
    },
    // 處理多媒體文件
    {
      test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
      loader: 'url-loader',
      options: {
        limit: 10000,
        name: 'static/assets/media/[name].[hash:7].[ext]',
      },
    },
    // 處理字體文件
    {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        loader: 'url-loader',
        options: {
            limit: 10000,
            name: 'static/assets/fonts/[name].[hash:7].[ext]'
        }
    },
  2. html 頁面中引入的圖片( 或其它資源 ) ==> html-loader
    css 中的圖片 url-loader 處理便可,而 html 中 img 標籤引入的圖片,不作工做的狀況下: 圖片將不會被處理,路徑也不會被改寫,即最終編譯完成後這部分圖片是找不到的,怎麼辦? html-loader !( 這個時候你應該是 url-loader 和 html-loader 都配置了,因此 css 中圖片、頁面引入的圖片、css 中的字體文件、頁面引入的多媒體文件等, 通通都會在編譯時被處理 )。

    // html中引用的靜態資源在這裏處理,默認配置參數attrs=img:src,處理圖片的src引用的資源.
    {
        test: /\.html$/,
        loader: 'html-loader',
        options: {
            // 除了img的src,還能夠繼續配置處理更多html引入的資源(不能在頁面直接寫路徑,又須要webpack處理怎麼辦?先require再js寫入).
            attrs: ['img:src', 'img:data-src', 'audio:src'],
            minimize: false,
            removeComments: true,
            collapseWhitespace: false
        }
    }
  3. 有的時候, 圖片可能既不在 css 中, 也不在 html 中引入, 怎麼辦?

    import img from 'xxx/xxx/123.jpg' 或 let img = require('xxx/xxx/123.jpg')

    js 中引用 img,webpack 將會自動搞定它。

  4. 圖片等資源的訪問路徑問題:
    通過上面的處理,靜態資源處理基本沒有問題了,webpack 編譯時將會將文件打包到你指定的生成目錄,可是不一樣位置的圖片路徑改寫會是一個問題.
    所有經過絕對路徑訪問便可,在 output 下的 publicPath 填上適當的 server 端頭,來保證全部靜態資源文件路徑能被訪問到,具體要根據服務器部署的目錄結構來作修改。

    output: {
     path: path.resolve(__dirname, 'dist'), // 輸出目錄的配置,模板、樣式、腳本、圖片等資源的路徑配置都相對於它
     publicPath: '/', // 模板、樣式、腳本、圖片等資源對應的server上的路徑
    }

自動將打包 js 引入生成的 html 文件

html-webpack-plugin插件,配置:
const HtmlWebpackPlugin = require('html-webpack-plugin')
new HtmlWebpackPlugin({
  favicon: './src/assets/img/favicon.ico', // favicon路徑,經過webpack引入同時能夠生成hash值
  filename: './views/index.html', // 生成的html存放路徑,相對於path
  template: './src/views/index.html', // html模板路徑
  title: '首頁', // 頁面title
  meta: '', // 容許插入meta標籤,如=>meta: {viewport: 'width=device-width,initial-scale=1, shrink-to-fit=no'}
  inject: 'body', // js插入的位置,true/'head'/'body'/false
  hash: true, // 爲靜態資源生成hash值(js和css)
  chunks: ['vendors', 'index'], // 須要在此頁面引入的chunk,不配置就會引入全部頁面的資源
  minify: {
    // 壓縮html文件
    removeComments: true, // 移除html中的註釋
    collapseWhitespace: true, // 刪除空白符與換行符
  },
})

如何在 webpack 中引入未模塊化的庫,如:Zepto

script-loader 把咱們指定的模塊 JS 文件轉成純字符串, exports-loader 將須要的 js 對象 module.exports 導出, 以支持 import 或 require 導入.
  1. 安裝依賴包

    npm i script-loader exports-loader -D
  2. 配置

    {
      test: require.resolve('zepto'),
      loader: 'exports-loader?window.Zepto!script-loader'
    }
    以上是正常處理一個 _"能夠 NPM 安裝但又不符合 webpack 模塊化規範" 的庫, 例如其它庫 XX, 處理後能夠直接 import xx from XX 後使用; 可是, zepto 有點特殊, 默認 npm 安裝的包或者從 github clone 的包, 都是僅包含 5 個模塊, 其它如經常使用的 touch 模塊是未包含的, 想要正常使用還需作得更多._
  3. 怎樣拿到一個包含更多模塊的 zepto 包 ?

    a) 打包出一個包含更多須要模塊的 zepto 包
    從 github clone 官方的包下來, 找到名爲 make 的文件 ( 在 package.json 同級目錄 ), 用記事本打開, 找到這一行 modules = (env['MODULES'] || 'zepto event ajax form ie').split(' '), 應該是在第 41 行, 手動修改加入你想要引入的模塊, 而後保存;

    b) 在 make 文件同級目錄 => 右鍵打開終端或 git bash => 敲 npm i 安裝 zepto 源碼須要的 node 包 ( 這裏你應當是已經已安裝過 nodejs 了, 若是沒有, 安裝好後再作這一步 ), 等待安裝結束.

    c) 在剛纔打開的 終端/git bash 敲命令 npm run-script dist, 若是沒有報錯, 你應該在這個打開的文件夾裏能夠看到生成了一個文件夾 dist, 打開它, 包含新模塊的 zepto 包就在這了, Over !

  4. 拿到新的 zepto 包後, 建議放到本身的 src 下 lib 目錄( 第三方工具包目錄 ), 再也不經過 npm 的方式去安裝和更新 zepto 了 ( 由於未來 npm update 後的 zepto 又將缺乏模塊,未來別人也會出現誤操做 ); 如今開始對這個放在 lib 目錄下的 zepto.min.js 進行處理:

    a) 經過 script-loader、exports-loader 轉成符合 webpack 模塊化規範的包

    {
       // # require.resolve()是nodejs用來查找模塊位置的方法,返回模塊的入口文件
       test: require.resolve('./src/js/lib/zepto.min.js'),
       loader: 'exports-loader?window.Zepto!script-loader'
    }

    b) 給模塊配置別名

    resolve: {
       alias: {
           'zepto': path.resolve(__dirname, './src/js/lib/zepto.min.js')
       }
    }

    c) 自動加載模塊, 再也不處處 import 或 require

    new webpack.ProvidePlugin({
      $: 'zepto',
      Zepto: 'zepto',
    })

    大功告成, 如今使用 zepto 跟你使用 jquery 或其它 node 包是同樣的開發體驗了 !

    以上, 演示的是對於一個第三方庫( 不能 npm 安裝,也不符合 webpack 規範 ), 如何去處理, 達到和正常 npm 安裝同樣的開發體驗, 僅就 zepto 來講, npm 庫有符合 webpack 規範的不一樣版本 ( zepto-webpack, 或 zepto-modules), 有須要能夠試試.
    平時意圖使用某個包, 先去 NPM 官網搜一搜比較好.

打包時排除應用中的某些模塊

某些時候,應用中依賴了某些模塊,但但願將這些模塊獨立經過CDN引入,以減少包的體積,因此沒必要將這些模塊打包,例如:jQuery。特定場景下,這個功能會有用武之地!
module.exports = {
  ...
  output: {
    ...
  },
  externals: {
    jquery: "jQuery"
  },
  ...
}

使用webpack打包js庫

一般打包js庫會選擇rollup,可是webpack一樣能夠作到,若是是須要對css、圖片等有較多應用的js庫,webpack會有更多優點,因rollup對樣式、圖片的處理能力是比較弱的。
  1. 配置

    打包出全部環境均可以使用的包—— umd
    module.exports = {
      ...
      entry: {
        sdk: 'xxxxxxx.js',
      },
      output: {
        ...
        library: '[name]',
        libraryTarget: 'umd',
        libraryExport: 'default', 
        umdNamedDefine: true, // 會對 UMD 的構建過程當中的 AMD 模塊進行命名,不然就使用匿名的 define
      },
      ...
    }
  2. 應用導出

    export default {
      a: xxxx,
      b: xxxx,
      c: xxxx,
    }
  3. 打包出的js,將支持import、requrie導入,script標籤導入,能夠經過window.sdk使用等:

    // import
    import { a, b, c } from '........js'
    
    // require
    const anything = require('........js')
    
    // window
    window.sdk
    window.sdk.a
    
    // node
    global.sdk
    global.sdk.a
  4. 知識擴展:

    1. 怎樣打包一個library?
    2. 一次打包暴露多個庫

配置開發服務器,webpack-dev-server

  1. 安裝依賴包

    npm i webpack-dev-server -D
  2. 經常使用配置

    devServer: {
      contentBase: path.join(__dirname, 'static'),    // # 告訴服務器從哪裏提供內容(默認當前工做目錄)
      host: 'localhost',  // # 默認localhost,想外部可訪問用'0.0.0.0'
      openPage: 'views/index.html',  // # 指定默認啓動瀏覽器時打開的頁面
      index: 'views/index.html',  // # 指定首頁位置
      port: 9090, // # 默認8080
      inline: true, // # 能夠監控js變化
      hot: true, // # 熱啓動
      open: true, // # 自動打開瀏覽器
      compress: true,  // # 一切服務都啓用gzip 壓縮
      watchContentBase: true  // # contentBase下文件變更將reload頁面(默認false)
    }
  3. 運行命令 ( package.json 配置命令 => npm run dev )

    "dev": "cross-env BUILD_ENV=development webpack-dev-server --mode development --colors --profile"
    根據目錄結構的不一樣, contentBase、openPage 參數要配置合適的值, 不然運行時應該不會馬上訪問到你的首頁; 同時要注意你的 publicPath, 靜態資源打包後生成的路徑是一個須要思考的點, 這與你的目錄結構有關。

配置 node express 服務,訪問打包後資源

某些時候,你可能想要build出前端代碼後,直接在本地訪問看看結果。能夠經過修改publicPath來變動靜態資源引用路徑,或者起一個本地服務來訪問。
  1. 新建 prod.server.js 文件

    let express = require('express')
    let compression = require('compression')
    
    let app = express()
    let port = 9898
    
    app.use(compression())
    app.use(express.static('./static/'))
    
    module.exports = app.listen(port, function(err) {
      if (err) {
        console.log(err)
        return
      }
      console.log('Listening at http://localhost:' + port + '\n')
    })
  2. 運行命令

    node prod.server.js
  3. 訪問路徑

    localhost:9898/views/

http-server

比本身配置一個 express 服務更簡潔的方式,去訪問打包後的資源。
  1. 安裝依賴

    npm i http-server -D
  2. package.json 配置命令

    "scripts": {
       "http-server": "http-server dist"
    }
  3. 運行命令

    npm run http-server
  4. 訪問路徑

    localhost:8080 或 http://127.0.0.1:8080

集成eslint

  1. 安裝依賴

    npm i eslint eslint-loader eslint-friendly-formatter babel-eslint -D
    eslint-friendly-formatter,指定終端中輸出eslint提示信息的格式。
  2. 增長配置

    {
        test: /\.js$/,
        enforce: 'pre',
        loader: 'eslint-loader',
        include: [paths.appSrc],
        exclude: [
          /node_modules/,
        ],
        options: {
          formatter: require('eslint-friendly-formatter'),
        },
      },
  3. package.json文件同級增長文件.eslintrc.js

    module.exports = {
        "root": true, 
        "parserOptions": {
            "sourceType": "module",
        },
        "parser": "babel-eslint", // eslint未支持的js新特性先進行轉換
        "env": {
            "browser": true,
            "es6": true,
            "node": true,
            "shared-node-browser": true,
            "commonjs": true,
        },
        "globals": {    // 設置全局變量(false:不容許重寫;)
            "BUILD_ENV": false,
        },
        "extends": "eslint:recommended", // 使用官方推薦規則,使用其餘規則,須要先install,再指定。
        "rules": {
            
        }
    }

    配置項含義:

    • root 限定配置文件的使用範圍
    • parser 指定eslint的解析器
    • parserOptions 設置解析器選項
    • extends 指定eslint規範
    • plugins 引用第三方的插件
    • env 指定代碼運行的宿主環境
    • rules 啓用額外的規則或覆蓋默認的規則
    • globals 聲明在代碼中的自定義全局變量
  4. ESLint官方的rules列表
  5. 若是有須要跳過檢查的文件/文件夾,新建.eslintignore文件

    /node_modules
  6. 參考文檔

    1. webpack引入eslint詳解
    2. babel-eslint

常見性能優化

  1. 使用happypack來優化,多進程運行編譯,參考文檔:

    1. webpack 優化之 HappyPack 實戰
    2. happypack 原理解析
  2. 使用cache-loader緩存編譯結果
  3. DllPlugin拆分基礎包



參考文檔

相關文章
相關標籤/搜索