共计 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
- 普通 H5 开发中, 引入组件化;
- 引入 art-template 前端渲染引擎——目前前端模版里速度最快;
- 配置 dev-server 调试模式,proxy 代理接口调试;
- 配置 watch 模式,方便在生产环境中用 Charles 映射到本地文件;
- optimization 配置提取 runtime 代码;
- splitChunks 拆分代码,提取 vendor 主要缓存包,提取 common 次要缓存包;
- 支持多页、多入口,自动扫描,可无限层级嵌套文件夹;
- 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 的选择?
- 通常使用 NPM 做包管理,但此项目使用 Yarn,因为 Yarn 有:速度快、可离线安装、锁定版本、扁平化等更多优点。
- 如果需要从 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] // 升级依赖包,指定版本
参考文档
- yarn 中文网
-
yarn 安装
(预警:如果本机已经安装过NodeJS
,使用brew
安装yarn
时,推荐使用brew install yarn --without-node
命令,否则可能导致其它 bug。) - yarn 命令
- yarn 和 npm 命令对比
Babel 转码
这是最新的 babel 配置,和网上的诸多教程可能有不同,可以自行测试验证有效性。
-
基础依赖包
npm i babel-loader@8 @babel/core -D
从 babel7 开始,所有的官方插件和主要模块,都放在了 @babel 的命名空间下。从而可以避免在 npm 仓库中 babel 相关名称被抢注的问题。
-
在 package.json 同级添加.babelrc 配置文件,先空着。
{"presets": [], // 预设 "plugins": [] // 插件}
-
package.json 文件可以声明需要支持到的浏览器版本
- package.json 中声明的 browserslist 可以影响到 babel、postcss,babel 是优先读取.babelrc 文件中 @babel/preset-env 的 targets 属性,未定义会读取 package.json 中的 browserslist。
为了统一,在 package.json 中定义。 -
package.json 中定义(推荐)
"browserslist": [ "> 1%", "last 2 versions", "not ie <= 8" ],
更多定义格式请查看:browserslist
-
.babelrc 中定义(不推荐)
{ "presets": [ [ "@babel/preset-env", { "targets": { "chrome": "58", "ie": "11" } } ] ] }
- package.json 中声明的 browserslist 可以影响到 babel、postcss,babel 是优先读取.babelrc 文件中 @babel/preset-env 的 targets 属性,未定义会读取 package.json 中的 browserslist。
- Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转码。
需要转译新的 API,使用@babel/preset-env
或@babel/plugin-transform-runtime
,二选一即可。 -
使用 @babel/preset-env
-
安装依赖包:
npm i @babel/preset-env @babel/polyfill -D
-
.babelrc 文件写上配置,@babel/polyfill 不用写入配置,会自动被调用。
{ "presets": [ [ "@babel/preset-env", { "useBuiltIns": "entry", "modules": false, } ] ] }
-
配置参数
-
modules
参数,"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false
,默认值是 auto。
用来转换 ES6 的模块语法。如果使用 false,将不会对文件的模块语法进行转化。
如果要使用 webpack 中的一些新特性,比如 tree shaking 和 sideEffects,就需要设置为 false,对 ES6 的模块文件不做转化,因为这些特性只对 ES6 的模块有效。 -
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‘)这种句法,对这类原型链上的句法问题不会做转译,书写代码需注意)
-
-
targets
参数,用来配置需要支持的的环境,不仅支持浏览器,还支持 node。如果没有配置 targets 选项,就会读取项目中的 browserslist 配置项。 -
loose
参数,默认值是 false,如果 preset-env 中包含的 plugin 支持 loose 的设置,那么可以通过这个字段来做统一的设置。
-
-
-
使用 @babel/plugin-transform-runtime
-
安装依赖包
npm i @babel/plugin-transform-runtime -D
-
如果配置参数 corejs 未设置或为 false,需安装依赖
@babel/runtime
(这部分代码会被抽离并打包到应用 js 里,所以可以安装在 dependencies 里),仅对 es6 语法转译,而不对新 API 转译。npm i @babel/runtime
-
如果配置参数 corejs 设置为 2,需安装依赖
@babel/runtime-corejs2
(同上,推荐安装在 dependencies 里。),对语法、新 API 都转译。npm i @babel/runtime-corejs2
- 推荐使用
corejs:2
,但是,检测不到‘hello‘.includes(‘h‘)
这种句法,所以存在一定隐患,书写代码时需注意。 - @babel/runtime 和 @babel/runtime-corejs2 这两个库唯一的区别是:corejs2 这个库增加了对 core-js(用来对 ES6 各个语法 polyfill 的库)这个库的依赖,所以在 corejs 为 false 的情况下,只能做语法的转换,并不能 polyfill 任何新 API。
-
-
.babelrc 文件写上配置
{"presets": [], "plugins": [ [ "@babel/plugin-transform-runtime", {"corejs": 2 // 推荐} ] ] }
-
配置参数
-
corejs
,默认值是 false,只对语法进行转换,不对新 API 进行处理;当设置为 2 的时候,需要安装@babel/runtime-corejs2
,这时会对 api 进行处理。 -
helpers
,默认值是 true,用来开启是否使用 helper 函数来重写语法转换的函数。 -
useESModules
,默认值是 false,是否对文件使用 ES 的模块语法,使用 ES 的模块语法可以减少文件的大小。
-
-
-
@babel/preset-env
还是@babel/plugin-transform-runtime
?(传送门:babel polyfill 和 runtime 浅析)-
@babel/preset-env + @babel/polyfill
可以转译语法、新 API,但存在污染全局问题; -
@babel/plugin-transform-runtime + @babel/runtime-corejs2
,可按需导入,转译语法、新 API,且避免全局污染(babel7 中 @babel/polyfill 是 @babel/runtime-corejs2 的别名),但是检测不到‘hello‘.includes(‘h‘)这种句法; -
@babel/polyfill 和 @babel/runtime-corejs2 都使用了 core-js(v2)这个库来进行 api 的处理。
core-js(v2)这个库有两个核心的文件夹,分别是 library 和 modules。@babel/runtime-corejs2 使用 library 这个文件夹,@babel/polyfill 使用 modules 这个文件夹。- library 使用 helper 的方式,局部实现某个 api,不会污染全局变量;而 modules 以污染全局变量的方法来实现 api;
-
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;
- 可以看出,library 下的这个 $export 方法,会实现一个 wrapper 函数,防止污染全局变量。
-
例如对 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();
- 从上面这个例子可以看出,对于 Promise 这个 api,@babel/polyfill 引用了 core-js/modules 中的 es6.promise.js 文件,因为是对全局变量进行处理,所以赋值语句不用做处理;@babel/runtime-corejs2 会生成一个局部变量_promise,然后把 Promise 都替换成_promise,这样就不会污染全局变量了。
-
综合上面的分析,得出结论:
-
如果是自己的应用 =>
@babel/preset-env + @babel/polyfill
-
useBuiltIns
设置为entry
比较不错。
在 js 代码第一行import '@babel/polyfill'
,或在 webpack 的入口 entry 中写入模块@babel/polyfill
,会将 browserslist 环境不支持的所有垫片都导入;
能够覆盖到‘hello‘.includes(‘h‘)
这种句法,足够安全且代码体积不是特别大,推荐使用! -
useBuiltIns
设置为usage
。
项目里不用主动 import,会自动将代码里已使用到的、且 browserslist 环境不支持的垫片导入;
相对安全且打包的 js 体积不大,但是,通常我们转译都会排除node_modules/
目录,如果使用到的第三方包有个别未做好 ES6 转译,有遇到 bug 的可能性,并且检测不到‘hello‘.includes(‘h‘)
这种句法。
代码书写规范,且信任第三方包的时候,可以使用! -
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": []}
-
-
如果是开发第三方类库 =>
@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 // 推荐} ] ] }
-
-
-
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" ] }
-
配置装饰器语法支持
-
安装依赖
npm i @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties -D
-
.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(更简洁)} ] ] }
-
-
配置 import 动态导入支持
-
安装依赖
npm i @babel/plugin-syntax-dynamic-import -D
-
.babelrc 文件增加配置
{"presets": [], "plugins": ["@babel/plugin-syntax-dynamic-import",] }
-
CSS 样式的处理(less 预编译和 postcss 工具)
-
需要安装的依赖包
npm i less less-loader css-loader style-loader postcss-loader postcss-import postcss-cssnext cssnano autoprefixer -D
-
配置
默认会将 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', })
-
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 了。
图片、字体、多媒体等资源的处理
-
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]' } },
-
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 } }
-
有的时候, 图片可能既不在 css 中, 也不在 html 中引入, 怎么办?
import img from 'xxx/xxx/123.jpg' 或 let img = require('xxx/xxx/123.jpg')
js 中引用 img,webpack 将会自动搞定它。
-
图片等资源的访问路径问题:
经过上面的处理,静态资源处理基本没有问题了,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 来配置对应的页面模版,是否可以自动扫描? 无论多少个入口,只管新建,而不用管理入口配置?可以的!
-
安装 node 模块 glob (扫描文件就靠它了).
npm i glob -D
const glob = require('glob')
-
自动扫描获取入口文件、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 }
-
webpack 打包入口
module.exports = {entry: utils.getEntries(), }
-
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 导入.
-
安装依赖包
npm i script-loader exports-loader -D
-
配置
{test: require.resolve('zepto'), loader: 'exports-loader?window.Zepto!script-loader' }
以上是正常处理一个 _” 可以 NPM 安装但又不符合 webpack 模块化规范 ” 的库, 例如其它库 XX, 处理后可以直接 import xx from XX 后使用; 但是, zepto 有点特殊, 默认 npm 安装的包或者从 github clone 的包, 都是仅包含 5 个模块, 其它如常用的 touch 模块是未包含的, 想要正常使用还需做得更多._
- 怎样拿到一个包含更多模块的 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 !
-
拿到新的 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
-
配置
module.exports = { ... entry: {sdk: 'xxxxxxx.js',}, output: { ... library: '[name]', libraryTarget: 'umd', libraryExport: 'default', umdNamedDefine: true, // 会对 UMD 的构建过程中的 AMD 模块进行命名,否则就使用匿名的 define }, ... }
-
应用导出
export default { a: xxxx, b: xxxx, c: xxxx, }
-
打包出的 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
-
知识扩展:
- 怎样打包一个 library?
- 一次打包暴露多个库
配置开发服务器,webpack-dev-server
-
安装依赖包
npm i webpack-dev-server -D
-
常用配置
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) }
-
运行命令 (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 来变更静态资源引用路径,或者起一个本地服务来访问。
-
新建 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') })
-
运行命令
node prod.server.js
-
访问路径
localhost:9898/views/
http-server,比自己配置一个 express 服务更简洁的方式,去访问打包后的资源。
-
安装依赖
npm i http-server -D
-
package.json 配置命令
"server": "http-server static"
-
访问路径
localhost:8080 或 http://127.0.0.1:8080
集成 eslint
-
安装依赖
npm i eslint eslint-loader eslint-friendly-formatter babel-eslint -D
eslint-friendly-formatter,指定终端中输出 eslint 提示信息的格式。
-
增加配置
{ test: /\.js$/, enforce: 'pre', loader: 'eslint-loader', include: [paths.appSrc], exclude: [/node_modules/,], options: {formatter: require('eslint-friendly-formatter'), }, },
-
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 声明在代码中的自定义全局变量
- ESLint 官方的 rules 列表
-
如果有需要跳过检查的文件 / 文件夹,新建
.eslintignore
文件/node_modules
-
参考文档
- webpack 引入 eslint 详解
- babel-eslint
常见性能优化
-
使用 happypack 来优化,多进程运行编译,参考文档:
- webpack 优化之 HappyPack 实战
- happypack 原理解析
- 使用 cache-loader 缓存编译结果
- DllPlugin 拆分基础包
参考文档
- webpack 中文文档 —— 直接阅读它非常有用,百度出来的教程 99% 都是管中窥豹,只见一斑,会形成误导(不要问我是怎么知道的 -_-)。
- NPM 中文文档
- 基于 webpack 的前端工程化开发之多页站点篇(一)
- 基于 webpack 的前端工程化开发之多页站点篇(二)
- webpack 在前端项目中使用心得一二
- webpack4 配置详解之逐行分析
- 手摸手,带你用合理的姿势使用 webpack4(上)
- 手摸手,带你用合理的姿势使用 webpack4(下)
- 一文读懂 babel7 的配置文件加载逻辑
- babel polyfill 和 runtime 浅析