前言
这是一篇关于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 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] // 升级依赖包,指定版本
参考文档
- 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.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;
- 可以看出,library下的这个$export方法,会实现一个wrapper函数,防止污染全局变量。
例如对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();
- 从上面这个例子可以看出,对于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 -Dnpm 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配置中的publickPathnew 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使用等:
// importimport { a, b, c } from '........js'// requireconst anything = require('........js')// windowwindow.sdkwindow.sdk.a// nodeglobal.sdkglobal.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 = 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')})
运行命令
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 浅析