Webpack-4手工搭建深度分析

32次阅读

共计 23207 个字符,预计需要花费 59 分钟才能阅读完成。

前言

这是一篇关于 webpack 4 手工搭建重点问题的分析,webpack 3 相关可以戳这里:https://github.com/Eleven90/webpack-pages-V3,这里并不试图从零手把手去堆代码,而是对其中的重点问题做稍微深入一点的解读。某些细节这里如果没有提及,项目代码里可能会有。

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

为懒人准备的 webpack 配置模版,可以直接用于生产。这里单纯只做 webpack 打包的配置、前端工程化代码的组织等,有意抛开三大框架,从原始的 H5 出发去组织代码,关于 React、Vue 等配置并不复杂,可以在开发需要时添加。

技术栈

es6 + less + art-template + webpack 4

  1. 普通 H5 开发中, 引入组件化;
  2. 引入 art-template 前端渲染引擎——目前前端模版里速度最快;
  3. 配置 dev-server 调试模式,proxy 代理接口调试;
  4. 配置 watch 模式,方便在生产环境中用 Charles 映射到本地文件;
  5. optimization 配置提取 runtime 代码;
  6. splitChunks 拆分代码,提取 vendor 主要缓存包,提取 common 次要缓存包;
  7. 支持多页、多入口,自动扫描,可无限层级嵌套文件夹;
  8. MockJs 模拟 mock 数据;

运行命令

推荐使用 yarn 进行包管理,项目在某个时间节点被我切换到了 yarn,但写文档时仍然是 npm,对应变更一下即可。

# 如果不熟悉或不想尝试 yarn,直接 npm install,然后 npm run dev 即可,不会有任何副作用
yarn / yarn install    # 安装全部依赖包

yarn dev               # 启动本地调试
yarn dev-mock          # 启动本地调试,MockJs 模拟接口数据
yarn dev:page-a        # 启动本地调试,仅 page- a 页面
yarn dev:page-b        # 启动本地调试,仅 page- b 页面

yarn build-dev         # 打包代码,publicPath 以 / 打头(可通过本地起服务访问 build 后的代码)yarn http-server       # 启动 http-server 服务器,可用来访问 yarn build-dev 打包的代码
yarn build-test        # 打包测试环境代码
yarn build             # 打包生产环境代码

# watch 模式,移除了 js、css 的压缩,节省时间(watch 时需要 build 压缩版代码,可自行修改)。yarn watch-dev         # 启动 watch 模式,本地开发环境(通常用不上)yarn watch-test        # 启动 watch 模式,测试环境
yarn watch             # 启动 watch 模式,生产环境
# 如果你想用 npm 的话...(建议把 yarn.lock 文件也删掉)npm install               # 安装全部依赖包

npm run dev               # 启动本地调试
npm run dev-mock          # 启动本地调试,MockJs 模拟接口数据
npm run dev:page-a        # 启动本地调试,仅 page- a 页面
npm run dev:page-b        # 启动本地调试,仅 page- b 页面

npm run build-dev         # 打包代码,publicPath 以 / 打头(可通过本地起服务访问 build 后的代码)npm run http-server       # 启动 http-server 服务器,可用来访问 npm run build-dev 打包的代码
npm run build-test        # 打包测试环境代码
npm run build             # 打包生产环境代码

# watch 模式,移除了 js、css 的压缩,节省时间(watch 时需要 build 压缩版代码,可自行修改)。npm run watch-dev         # 启动 watch 模式,本地开发环境(通常用不上)npm run watch-test        # 启动 watch 模式,测试环境
npm run watch             # 启动 watch 模式,生产环境

Yarn 和 NPM 的选择?

  1. 通常使用 NPM 做包管理,但此项目使用 Yarn,因为 Yarn 有:速度快、可离线安装、锁定版本、扁平化等更多优点。
  2. 如果需要从 npm 切换到 yarn,或者从 yarn 切换到 npm 时,请整体移除 node_modules 目录,及 yarn.lock/package-lock.json 文件,因 yarn 和 npm 两者的策略不同,导致相同的 package.json 列表安装后的 node_modules 结构是不同的(虽然这并不会引发 bug,但建议在切换时清除后重新install)。

Yarn 常用命令

yarn / yarn install                 // 安装全部(package.json)依赖包 —— npm install

yarn run [dev]                      // 启动 scripts 命令

yarn add [pkgName]                  // 安装依赖包(默认安装到 dependencies 下)—— npm install [pkgName]
yarn add [pkgName]@[version]        // 安装依赖包,指定版本 —— npm install [pkgName]@[version]
yarn add [pkgName] -D               // 安装依赖包,指定到 devDependencies —— npm install [pkgName] -D
yarn remove [pkgName]               // 卸载依赖包 —— npm uninstall [pkgName]

yarn upgrade [pkgName]              // 升级依赖包 —— npm update [pkgName]
yarn upgrade [pkgName]@[version]    // 升级依赖包,指定版本
参考文档
  1. yarn 中文网
  2. yarn 安装
    (预警:如果本机已经安装过 NodeJS,使用brew 安装 yarn 时,推荐使用 brew install yarn --without-node 命令,否则可能导致其它 bug。)
  3. yarn 命令
  4. yarn 和 npm 命令对比

Babel 转码

这是最新的 babel 配置,和网上的诸多教程可能有不同,可以自行测试验证有效性。

  1. 基础依赖包

    npm i babel-loader@8 @babel/core -D

    从 babel7 开始,所有的官方插件和主要模块,都放在了 @babel 的命名空间下。从而可以避免在 npm 仓库中 babel 相关名称被抢注的问题。

  2. 在 package.json 同级添加.babelrc 配置文件,先空着。

    {"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 中定义(推荐)

      "browserslist": [
        "> 1%",
        "last 2 versions",
        "not ie <= 8"
      ],

      更多定义格式请查看:browserslist

    3. .babelrc 中定义(不推荐)

      {
        "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-env

    1. 安装依赖包:

      npm i @babel/preset-env @babel/polyfill -D
    2. .babelrc 文件写上配置,@babel/polyfill 不用写入配置,会自动被调用。

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

      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。

        • 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",]
      }

CSS 样式的处理(less 预编译和 postcss 工具)

  1. 需要安装的依赖包

    npm i less less-loader css-loader style-loader postcss-loader postcss-import postcss-cssnext cssnano autoprefixer -D
  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 文档
    • PostCSS 自学笔记(一)【安装使用篇】
    • 展望未来:使用 PostCSS 和 cssnext 书写 CSS
    • 使用 PostCSS+cssnext 来写 css
    • PostCSS 及其常用插件介绍

      使用时在 webpack.config.js 同级目录新建 postcss.config.js 文件:

    module.exports = {
      // 是一个以缩进为基础的语法,类似于 Sass 和 Stylus
      // https://github.com/postcss/sugarss
      // parser: 'sugarss',
      plugins: {'postcss-import': {},
        'postcss-cssnext': {},
        cssnano: {},},
    }

常用的插件:

  • autoprefixer ——_插件在编译时自动给 css 新特性添加浏览器厂商前缀, 因此, 借助 Modernizr 来添加厂商前缀已经不需要了(还是可以用来做 js 检测浏览器兼容性的)._
  • postcss-cssnext ——_让你使用下一代 css 语法, 在最新的 css 规范里, 已经包含了大量 less 的内置功能_
  • 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 入口文件和 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
    }

如何在 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 库

打包出所有环境都可以使用的包——umd

  1. 配置

    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 配置命令

    "server": "http-server static"
  3. 访问路径

    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": {
            "no-console": "off",
            "no-undef": "off",
            "no-useless-escape": "off",
        }
    }

    配置项含义:

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

参考文档

  • webpack 中文文档 —— 直接阅读它非常有用,百度出来的教程 99% 都是管中窥豹,只见一斑,会形成误导(不要问我是怎么知道的 -_-)。
  • NPM 中文文档
  • 基于 webpack 的前端工程化开发之多页站点篇(一)
  • 基于 webpack 的前端工程化开发之多页站点篇(二)
  • webpack 在前端项目中使用心得一二
  • webpack4 配置详解之逐行分析
  • 手摸手,带你用合理的姿势使用 webpack4(上)
  • 手摸手,带你用合理的姿势使用 webpack4(下)
  • 一文读懂 babel7 的配置文件加载逻辑
  • babel polyfill 和 runtime 浅析

正文完
 0