前言

这是一篇关于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 installyarn 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] -Dyarn 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.jsvar 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.jsvar 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/polyfillrequire("core-js/modules/es6.promise");var p = new Promise();// @babel/runtime-corejs2var _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 -Dnpm 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配置中的publickPathnew 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使用等:

    // importimport { a, b, c } from '........js'// requireconst anything = require('........js')// windowwindow.sdkwindow.sdk.a// nodeglobal.sdkglobal.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 = 9898app.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 浅析