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拆分基础包



参考文档

相关文章
相关标签/搜索