什么是webpack
webpack4 可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JS模板以及其他的一些浏览器不能直接运行的拓展语言(Scss, Ts)等,并将其打包为合适的格式以供浏览器使用。
webpack可以做什么
- 代码转换: es6转es5,sccc、less转css等
- 文件优化: 压缩代码体积,合并文件,摇树优化等
- 代码分割: 公共模块抽离,路由懒加载等
- 模块合并: 多个模块合并成一个模块,按功能来分类
- 自动刷新: 自己启动一个本地服务,来实现代码变更后,可以更新我们的页面
- 代码校验: 校验代码是否符合规范
- 自动发布: 打包结果发布到服务器上,后面提到的方法
webpack安装
安装本地的webpack
yarn add webpack@^4.32.2 webpack-cli@^3.3.2 -D
// 可以查看 npm 源
yarn global add nrm
// 查看 npm 源
nrm ls
// 修改源
nrm use cnpm
webpack可以进行零配置
webpack默认支持js、json,所以我们可以零配置打包js代码,默认会找src/index.js作为入口文件。
- src
- index.js
// index.js
console.log('index start');
然后执行 npx webpack, 就在我们的dist/文件夹生成了一个打包后的main.js,此时默认采用mode为生产环境(production),会自动优化、压缩。
手动配置webpack
比如我们想更改mode为开发环境,希望打包后的代码不被压缩,我们可以手动配置webpack。默认配置文件的名字为 webpack.config.js或 webpackfile.js,其实扒开 webpack-cli 中就能找到这样一段代码,
// node_modules/webpack-cli/bin/config/config.yargs.js
defaultDescription: "webpack.config.js or webpackfile.js"
如果我们期望更改webpack启动配置文件的名称,可以通过 npx webpack --config someName.js来来指定文件。
新建文件 src/a.js,webpack.conf.my.js 并修改代码如下
// src/a.js
export default {
a: 1
}
// src/index.js
import obj from './a.js';
console.log(obj);
// webpack.conf.my.js
let path = require('path');
export default {
mode: 'development', // 更清晰的打包结果
entry: './src/index.js', // 入口
output: {
filename: 'bundle.js', // 打包后的文件名
path: path.resolve(__dirname, 'build') // 路径必须是个绝对路径
}
}
package.json中增加build命令
{
...,
"scripts": {
"build": "npx webpack --config webpack.conf.my.js"
},
...
}
分析打包后的文件
执行npm run build (如果需要额外参数 需要加一个 --),生成打包后的文件 build/build.js, 我们来分析下这个文件
CODE
// webpack自执行启动函数 入参是一个对象 { './src/a.js': function, './src/index.js': function }
(function(modules) {
// 已缓存的模块 非首次加载的模块从这里面拿
var installedModules = {};
// 实现了一个require方法 接收一个模块id 模块id即是路径 类似 './src/a.js'
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
// 缓存中存在该模块,直接返回该模块
return installedModules[moduleId].exports;
}
// 声明一个新的 module 对象 并在缓存对象中缓存
var module = installedModules[moduleId] = {
i: moduleId, // 模块id
l: false, // 模块状态 是否加载成功
exports: {} // 模块导出
};
// 使用传入的 moduleId 对应的函数解析模块
// + 把 module.exports 这个对象当成 this
// + 把 module 传入函数
// +「首次加载时,module 就是 { i: moduleId,l: false, exports: {} }」
// + 把 module.exports 传入函数「首次加载时,就是 {}」
// + 把 __webpack_require__ 作为实参传给函数使用,用于递归引用
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded 更改模块加载状态
module.l = true;
// Return the exports of the module 导出模块返回结果
return module.exports;
}
// __webpack_require__ 上挂载全部传入的 modules 参数
__webpack_require__.m = modules;
// __webpack_require__ 上挂载 cache 的模块对象
__webpack_require__.c = installedModules;
// 给每个 exports 出的对象设置可枚举和可取值属性
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// 为每个模块 exports 出对象设置 Symbol.toStringTag
// 也就是 Object.toString.call(exports) 返回的[object Module]
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
__webpack_require__.t = function(value, mode) {
if(mode & 1) value = __webpack_require__(value);
if(mode & 8) return value;
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
return ns;
};
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
// __webpack_public_path__
__webpack_require__.p = "";
// Load entry module and return exports 函数默认值 不传的话为src/index.js 加载并return出加载到的模块exports的值
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({ "./src/a.js": (function(module, exports) {
eval("module.exports = {\n a: 1\n}\n\n//# sourceURL=webpack:///./src/a.js?");
}),
"./src/index.js": (function(module, exports, __webpack_require__) {
// 注意这里 obj接收了 __webpack_require__的返回值,也就是对应模块的exports,所以是{ a: 1 },然后打印obj 得出结果
eval("var obj = __webpack_require__(/*! ./a.js */ \"./src/a.js\");\n\nconsole.log(obj);\n\n//# sourceURL=webpack:///./src/index.js?");
})
});
- webpack启动函数是一个自执行函数,接收参数为{ 模块路径1: function, 模块路径2: function }这样一个对象,内部实现了自己require方法,先分析的入口文件问src/index.js,代码中遇到模块加载,则继续调用__webpack_require__方法加载模块。
- 加载过的文件会被缓存,第二次加载直接从内存中的缓存对象中拿。
- 任何打包进build.js中的模块,都有三个属性,{ 模块id,模块加载状态,模块exports }, 入口文件的导入的其实是其他模块的module.exports。
配置环境变量
- webpack 中的 mode 字段,会在浏览器环境设置 process.env.NODE_ENV,但是 node 环境无法通过 process.env.NODE_ENV 访问(也可以启动命令通过 --mode 指定,优先级更高)。
- 启动命令增加 --env,会给 webpack 配置传入参数,webpack.config.js 需要导出一个函数,第一个参数为 env,第二个参数为 { env },如果传入 --env=development,那么 env.development = true,通常用于区分环境,加载不同的 webpack 配置。
- 在 node 环境设置变量怎么办呢?其实在 mac 中可以通过 export,在 window 下用 set,而兼容两者则需要使用 cross-env 设置~
"build": "export NODE_ENV=production && webpack"
"build": "set NODE_ENV=production && webpack"
"build": "cross-env NODE_ENV=production webpack" // 注意不需要 && 符号啦
- 还有一种方法,我们可以写一个 .env 文件,利用 dotenv 来实现 process.env 属性的读写~
// 安装 dotenv 包
yarn add dotenv
// 解析 .env 文件
require('dotenv').config({ path: 'env' });
热启动 webpack-dev-server
内部通过express来实现的这样一个静态服务,注意这样会新启一个服务,如果想复用现有的 express 服务实例,可以使用 webpack-dev-middleware,其实 webpack-dev-server 内部也使用的 webpack-dev-middleware,手动创建了个 express 实例传进去了而已~
yarn add webpack-dev-server -D
webpack-dev-server把打包文件写在内存中,所以不会在我们build文件夹中体现,可以通过npx webpack-dev-server来启动一个服务,当然更多我们会用到一些配置,webpack配置文件增加以下配置项
devServer: { // webpack-dev-server配置
port: 3000, // 启动端口
progress: true, // 进度条
publicPath: '/', // 静态文件目录
contentBase: './build', // 以 build 目录作为额外的静态服务启动的文件
compress: true, // 是否启用压缩
open: true, // 自动打开浏览器
}
build文件加新增文件index.html
<!DOCTYPE html>
...
<script src="./bundle.js"></script>
</html>
配置启动命令
// package.json
"dev": "webpack-dev-server --config webpack.conf.my.js"
执行npm run dev,可以打开一个页面,并输出我们的结果。
html-webpack-plugin
我们并不希望在build文件夹中手动添加index.html,并手动引入资源,我们希望我们项目真正的html模板被打包,这时候,可以使用 html-webpack-plugin
yarn add html-webpack-plugin -D
src目录下新建index.html, 不用引入任何文件~
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack4 学习</title>
</head>
<body>
</body>
</html>
webpack配置中增加plugin配置
plugins: [ // 是一个数组 放着所有的webpack插件的实例
new HtmlWebpackPlugin({
template: './src/index.html', // 模板路径
filename: 'index.html', // 打包后的文件名
minify: {
removeAttributeQuotes: true, // 删除html中的属性双引号
// collapseWhitespace: true // 压缩成一行
},
hash: true // 引入资源路径后的hash值 比如./build?xxssss
})
]
打包结果
<!DOCTYPE html>
...
<script src=bundle.5cd64a02.js?5cd64a021942ccddb94f></script></body>
</html>
css-loader, style-loader
loader的作用就是对我们的源代码进行转换,使得原本 webpack 不认识的模块被正确解析和返回,要引入 css,需要正确的 loader将它转换成真正的 module。比如常用的css-loader,style-loader,需要注意它们的区别:
- css-loader 是用来解析 @import 和 url 的,并将css包装成一个模块,模块导出的是base.css的内容,然后webpack将@import替换成__webpack_require__,去加载这个模块
- style-loader 是把生成的css插入header标签中
- 可以use多个loader,方法是把use对应的value配置成数组
- loader是有顺序的,默认是从右向左执行,从下到上执行,比如我需要先处理css文件,然后把css内容插入到html的header标签的最底部,所以我需要把css-loader写后面, style-loader写在前面。
比如 index.css中引用 base.css,此时会打包出一个
// ./node_modules/css-loader/dist/cjs.js!./base.css
__webpack_exports__[\"default\"] = (___CSS_LOADER_EXPORT___);
并且在打包出的index.css文件中把@import替换成了
__webpack_require__("./node_modules/css-loader/dist/cjs.js!./base.css\");
配置style-loader, css-loader
module: { // 模块
rules: [ // 配置规则
{
test: /\.css$/,
use: [
{
'style-loader',
options: {
// 插入到head标签顶部 这里略
insert: function insertAtTop() {}
}
},
{
loader: 'css-loader', // url import 进行处理
options: {
modules: {
mode: 'local',
localIndentName: "[path][name]__[local]--[hash:base64:5]" // 指定 css module 生成规则
},
},
},
]
}
]
}
配置less-loader
less-loader 能帮我们处理less文件,把less文件转换成css文件,所以它应该在css-loader之前执行,根据单模块从右向左的执行顺序,less-loader应该配置在最右边。注意,less-loader会调用 less来进行语法转换,所以我们需要安装 less和 less-loader两个包。
yarn add less less-loader -D
更改配置
module: { // 模块
rules: [ // 配置规则
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader'
]
}
]
}
mini-css-extract-plugin 抽离css文件
现在所有的css都放在style标签中了(style-loader干的),能不能把他提取成一个文件,通过link标签去引用呢,ok,mini-css-extract-plugin 就是做这样一个事儿,它的loader也取代了style-loader,因为我们不再需要style标签引入css了。
yarn add mini-css-extract-plugin -D
改下配置文件
// 如果需要抽离成多个css 可以多次引用 不同的变量名即可
let MiniCssExtractPlugin = require('mini-css-extract-plugin');
// plgin中使用
module.exports = {
plugins: [
new MiniCssExtractPlugin({ // 抽离css
filename: '[name].css', // 抽离出的文件名
})
],
module: { // 模块
rules: [ // 配置规则
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader, // 把css抽离出 并link标签引入
'css-loader',
'less-loader'
]
}
]
}
}
看下打包后的文件
<!DOCTYPE html>
<html lang=en>
<head>
...
<link href=main.css?acd7a28111ac8e884862 rel=stylesheet></head>
<body>
<div id=study>
学习使我快乐
</div>
<script src=bundle.js?acd7a28111ac8e884862></script></body>
</html>
postcss-loader, autoprefixer 添加css前缀
我们使用autoprefixer来给css文件添加加各类浏览器兼容的前缀,以 Can I Use 上的 浏览器支持数据 为基础,自动处理兼容性问题,它需要搭配postcss-loader(后处理器)使用。
yarn add postcss-loader autoprefixer -D
修改webpack配置文件
module: { // 模块
rules: [ // 配置规则
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader, // 返回js,把css抽离出 并link标签引入
'css-loader',
'postcss-loader', // 在解析css为模块和解析css内@import之前使用
'less-loader'
]
}
]
}
根目录添加postcss.config.js 或者直接写进postcss-loader配置中
// postcss.config.js
module.exports = {
plugins: [require('autoprefixer')]
}
最后一步: 在package.json中增加配置,也可以根目录创建.browserslistrc文件
"browserslist": [
"last 2 versions",
"> 1%",
"iOS 7",
"last 3 iOS versions"
]
更:webpack5 可以使用 postcss-loader 配合 postcss-preset-env
let postCSSPresetEnv = require('postcss-preset-env');
module.exports = {
plugins:[
postCSSPresetEnv({ // 预设
browsers:'last 10 version'
})
]
}
uglifyjs-webpack-plugin, optimize-css-assetts-webpack-plugin (js,css压缩)
我们知道,当 mode为 production时,js会自动压缩,那 css不能压缩怎么办呢。
抽离css后,需要借助 optimize-css-assetts-webpack-plugin 帮我们压缩css,需要配置optimization.minimizer,然而配置之后,js又不能自动压缩了, 所以我们一般配合 uglify完成 css和 js的压缩任务。
let OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
let UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin'); // 压缩js 配置css压缩后 js自动压缩失效 需要用此插件进行压缩
module.exports = {
mode: 'production',
optimization: [ // 优化项 只在生产环境生效
new UglifyjsWebpackPlugin({ // 压缩js
cache: true, // 是否使用缓存
parallel: true, // 是否是并发压缩js的
sourceMap: true // 压缩完后 源码映射 为了更好调试
}),
new OptimizeCssAssetsPlugin(); // 压缩 css
]
}
webpack5 我们一般使用 optimize-css-assets-webpack-plugin 结合 terser-webpack-plugin 做代码的压缩。
babel-loader,@babel/core,@babel/preset-env (es6 转成 es5)
如果我们在a.js中加了一个箭头函数,然后在入口文件引入a.js,却发现打包后的 bundle.js里面还是箭头函数(webpack预设的js模块解析并没有对js进行语法转换,我们需要添加自己的loader去干这件事),这在一些不支持es6语法的浏览器中会报错的,为了解决这个问题,我们需要把高版本的 js按照es5标准进行转换。
yarn add babel-loader @babel/core @babel/preset-env -D
babel-loader: 识别 js 文件。 @babel/core: 生成 AST,调用transform方法去转换 js源代码。 @babel/preset-env:转化规则,把标准的语法转换为低级的语法。
webpack 增加 babel-loader 配置
module: { // 模块
rules: [ // 配置规则
{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: { // 用 babel-loader 把 es6转成 es5
presets: [ // 预设插件库
'@babel/preset-env'
]
}
}]
}
]
}
更快的 swc-loader
SWC 是一个类似于 Babel 的代码转义器,它的主要功能就是把 ES2015 或更高版本的 JS 代码转换为老浏览器能够使用的 ES5 或更低版本的 JS 代码。SWC 是使用 Rust 语言编写的,相比 Babel 来说,速度要更快。按照官网的说法 SWC 的速度要比 Babel 快 20 倍。
npm install --save-dev @swc/core swc-loader core-js
module: {
rules: [
// SWC 的 Loder 配置
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'swc-loader',
},
},
],
}
@babel/plugin-syntax-class-properties 解析class类提案语法
如果我们在 a.js中添加了以下这段豪横无比的代码,webpack会无情报错的。
class Person {
a = 1;
}
这时候,@babel/plugin-syntax-class-properties懂你
yarn add @babel/plugin-syntax-class-properties -D
更改webpack配置
module: { // 模块
rules: [ // 配置规则
{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: { // 用 babel-loader 把 es6转成 es5
presets: [ // 预设插件库 是大的插件的集合
'@babel/preset-env'
],
plugins: [ // 配置一个个的插件
'@babel/plugin-proposal-class-properties'
]
}
}]
}
]
}
@babel/plugin-proposal-decorators 解析类装饰器
@log
class Person {
a = 1
}
function log(target) {
console.log(target);
}
let ys = new Person();
我们给class类加一个修饰器,build的时候还是会报错的,因为修饰器提案想要进行语法转换也需要一个包,@babel/plugin-proposal-decorators
yarn add @babel/plugin-proposal-decorators -D
修改 babel-loader配置 此处参考babel官网提供的用法 babel-plugin-proposal-decorators
module: { // 模块
rules: [ // 配置规则
{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: { // 用 babel-loader 把 es6转成 es5
presets: [ // 预设插件库 是大的插件的集合
'@babel/preset-env'
],
plugins: [ // 配置一个个的插件
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose" : true }]
]
}
}]
}
]
}
@babel/plugin-transform-runtime @babel/runtime 注入运行时辅助代码
此时我们再给入口index.js增加
// index.js
class Person2 {
}
打包后我们看到,bundle.js里面声明了两次 _classCallCheck 方法,是否可以公用呢?
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\");
我们在index.js文件中继续添加
function* gen() {
yield 1;
}
console.log(gen().next());
然后npm run dev启动服务,打开页面,报错如下
Uncaught ReferenceError: regeneratorRuntime is not defined
wtf? 这个方法是哪里来的? 其实 Generator 或者更高级的 Promise 语法进行转化的时候,使用了 regeneratorRuntime 辅助方法却并没有加上这个辅助方法,需要增强编译,我们可以是使用 @babel/plugin-transform-runtime,这是一个代码运行时的包,会在代码运行时根据需要注入一些辅助性的代码。
@babel/plugin-transform-runtime是我们开发时候需要用的包,但是上线打包的话,我们也需要带上这些补丁代码,babel为我们提供了 @babel/runtime 去处理线上代码的打包。
yarn add @babel/plugin-transform-runtime -D
yarn add @babel/runtime // 生产环境需要用的 就不加-D
webpack增加配置,注意,这步操作要排除 node_modules 中的 js,仅包含 src目录下的 js 代码 这时候可以看到,打包出的源码中自动引入了@babel/runtime下的包啦
/***/ "./node_modules/@babel/runtime/helpers/classCallCheck.js": 'xxx'
/***/ "./node_modules/@babel/runtime/helpers/classCallCheck.js": 'xxx'
注,在 webpack5 中,我们使用 babel-plugin-transform-runtime 来完成这个功能。
高级 api 不好使? babel-polyfill 垫一下
- Babel默认只转换新的javascript语法,而不转换新的API,比如 Iterator, Generator, Set, Maps, Proxy, Reflect,Symbol,Promise 等全局对象。以及一些在全局对象上的方法(比如 Object.assign)都不会转码。
- 比如说,ES6在Array对象上新增了Array.form方法,Babel就不会转码这个方法,如果想让这个方法运行,必须使用 babel-polyfill来转换等
- babel-polyfill 它是通过向全局对象和内置对象的prototype上添加方法来实现的。比如运行环境中不支持Array.prototype.find方法,引入polyfill, 我们就可以使用es6方法来编写了,但是缺点就是会造成全局空间污染
- @babel/preset-env 为每一个环境的预设,@babel/preset-env 默认支持语法转化,需要开启useBuiltIns配置才能转化API和实例方法,useBuiltIns 选项可选值包括:"usage" | "entry" | false, 默认为 false,表示不对 polyfills 处理,这个配置是引入 polyfills 的关键。
使用 babel-polyfill
npm i @babel/polyfill
useBuiltIns": false 此时不对 polyfill 做操作。如果引入 @babel/polyfill,则无视配置的浏览器兼容,引入所有的 polyfill,86.4kb。
import '@babel/polyfill';
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [[
"@babel/preset-env",
{
+ useBuiltIns: false,
}
],
"@babel/preset-react"],
plugins: [
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }]
]
}
}
}
"useBuiltIns": "entry" 含义
- 在项目入口引入一次(多次引入会报错)
- "useBuiltIns": "entry" 根据配置的浏览器兼容,引入浏览器不兼容的 polyfill。需要在入口文件手动添加 import '@babel/polyfill',会自动根据 browserslist 替换成浏览器不兼容的所有 polyfill
- 这里需要指定 core-js 的版本, 如果 "corejs": 3, 则 import '@babel/polyfill' 需要改成 import 'core-js/stable';import 'regenerator-runtime/runtime';
corejs默认是2,配置2的话需要单独安装core-js@3
npm i core-js@3
import 'core-js/stable';
import 'regenerator-runtime/runtime';
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [["@babel/preset-env", {
+ useBuiltIns: 'entry',
+ corejs: { version: 2 }
}], "@babel/preset-react"],
plugins: [
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }]
]
}
}
},
{
"browserslist": {
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
],
"production": [
">1%"
]
},
}
"useBuiltIns": "usage" 含义
- "useBuiltIns": "usage",usage 会根据配置的浏览器兼容,以及你代码中用到的 API 来进行 polyfill,实现了按需添加
- 当设置为 usage 时,polyfills 会自动按需添加,不再需要手工引入 @babel/polyfill
- usage 的行为类似 babel-transform-runtime,不会造成全局污染,因此也会不会对类似 Array.prototype.includes() 进行 polyfill
import '@babel/polyfill';
console.log(Array.from([]));
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [["@babel/preset-env", {
+ useBuiltIns: 'usage',
+ corejs: { version: 3 }
}], "@babel/preset-react"],
plugins: [
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }]
]
}
}
},
最佳实践
babel-runtime 适合在组件和类库项目中使用,而 babel-polyfill 适合在业务项目中使用。
eslint 代码校验
手动配置
// eslint-loader 会调用 babel-eslint 转义高版本语法的代码
// 然后调用 eslint 进行代码检查
cnpm install eslint eslint-loader babel-eslint -D
修改 webpack.config.js
module: {
rules: [
{
test: /\.js$/, // 如果加载的模块是以 .js 结尾的
loader: 'eslint-loader', // 进行代码风格检查
enforce: 'pre', // pre loader
options: { fix: true }, // 如果发现不合要求,会自动修复
exclude: /node_modules/
},
// ...
]
}
增加 .eslintrc.js 文件
module.exports = {
root: true, // 配置文件可以有继承关系的,这里是根配置
parser: 'babel-eslint', // 把源代码解析为 AST 语法树的工具
parserOptions: {
sourceType: 'module',
ecmaVersion: 2015
},
// 指定脚本的运行环境
env: {
browser: true
},
rules: {
indent: 'off', // 缩进的风格
quotes: 'off', // 引号的类型
'no-console': 'error', // 不能出现 console
}
}
此时再打包,如果我们入口文件中有 console.log,就会有警告啦,但是我们手动配置了三个规则,这显然是不够的,而全写的话又太多了,我们得考虑继承自一个比较完善的配置,然后做二次修改。
使用 eslint-config-airbnb
eslint-config-airbnb 是目前比较好的业界实践,先来安装包(笔者这里测试代码使用了 react):
{
"eslint": "^7.28.0",
"eslint-loader": "^4.0.2",
"babel-eslint": "^10.1.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-import": "^2.23.4"
}
修改.eslintrc.js 文件
module.exports = {
// root: true, // 配置文件可以有继承关系的,这里是根配置
extends: 'airbnb', // 是继承自 airbnb 的配置
parser: 'babel-eslint', // 把源代码解析为 AST 语法树的工具
// 指定脚本的运行环境
env: {
browser: true,
node: true, // node 环境下也可以使用
},
rules: {
'linebreak-style': 'off', // 换行符检查关闭
indent: ['error', 2],
'no-console': 'off',
},
};
新建 .vscode/settings.json,用于约束编辑器行为(js 文件保存时自动修复错误)~
{
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
vscode 安装 eslint 插件,用于不规范代码提示,这个就不再赘述了~
测试代码
import React from 'react';
import ReactDOM from 'react-dom';
const a = 1;
console.log(a, React);
ReactDOM.render('hello', document.getElementById('root'));
npm run dev 就可以看到 a = 1 处标红,ctrl + s 保存代码时自动修复。
expose-loader在 window 挂载变量
很多第三方模块依赖jquery,而且依赖的是window.jquery,也就是全局的 jquery。
yarn add jquery
比如我们在a.js中增加如下代码
// a.js
import $ from 'jquery';
console.log(window.$, $, 'jquery'); // undefined fn jquery
我们看到打包后的文件里是有jquery的, jquery被包成一个闭包去执行,暴露出去,但是 window 上没有。如果有插件依赖了 window.jquery怎么办呢,别急,expose-loader可以暴露全局变量,注意,它是一个内联loader,
yarn add expose-loader -D
语法如下
// expose-loader?exposes[]=暴露到全局的别名!引用包名
import $ from 'expose-loader?exposes[]=$!jquery';
console.log(window.$, $, 'jquery'); // fn fn jquery
当然也可以不用内联loader,配到webpack文件里去,参考官方写法。
module: {
rules: [
{
test: require.resolve('jquery'), // 只想找路径,而不想加载并执行
loader: 'expose-loader',
options: {
exposes: ['$']
}
}
]
}
但是我们发现,这样不是很友好,能不能直接用,而不需要每个模块都去去 import 呢?
webpack.ProvidePlugin 声明全局包
可以使用webpack.ProvidePlugin去声明全局变量
let webpack = require('webpack');
module.exports = {
plugins: [
// 把包引为全局变量,需要注意的是,这种全局变量并不会挂到 window 上
new webpack.ProvidePlugin({
$: 'jquery' // 引入 node_modules/jquery 声明为 $, 每个模块中都注入,而不是挂在window
})
]
}
// index.js
consolle.log($); // fn
externals 忽略不需要被打包的模块(外部引入,模块内二次引入)
如果我在index.html通过 cdn 引入了 jquery,此时 window.jquery 即为 jquery,但是呢,我很任性,不希望用的时候写那么长的变量名,于是我做了以下操作
import $ from 'jquery';
这时一看打包文件,引入了双份的 jquery,我 emm..
我又比较有洁癖,只想引入一个包,怎么办呢? webpack提供了 externals 属性,它可以让我们打包时忽略已经外部引用的包。
externals: {
jquery: '$'
}
其实它内部打包的时候,会依次遍历 externals 内配置,比如 jquery,如果配置过,那么 jquery 包就会变成 module.export = window.jquery 啦。
file-loader 和更强大的 url-loader 进行图片处理
图片有几种引入方式
- 在 js 中创建图片引入
- css background 引入 file-loader 用来解析图片,file-loader 默认会在内部生成一张带hash的图片,到 build 目录下,并且把生成的图片路径返回回来。
yarn add file-loader -D
js中引入图片
新建img.js,并在 index.js 中引入它。
import logo from '../logo.png'; // 注意 返回的是一个图片地址
let image = new Image();
image.src = logo; // 思考,如果我们这里直接访问一个图片呢,比如 '../logo.png' 它会被打包么?
document.body.append(image);
增加loader规则
module: { // 模块
rules: [ // 配置规则
{
test: /\.(png|jpg|gif)$/,
use: 'file-loader'
},
],
}
ok, 大功告成
css中引入图片
// index.less
body {
background: url(./logo.png) no-repeat;
}
因为 css-loader 默认会把 css 中引入的图片,转换成一个外部模块,比如以上代码,实际上被 css-loader 转换成以下代码,所以他也会打包。
body {
background: url(require('./logo.png')) no-repeat;
}
假设我们有一个场景,如果图片小于 5k,就把它转成 base64(不会发请求,但是大小会比源文件大三分之一),否则用 file-loader产生真实的图片,防止过多的静态资源请求,这时候我们可以使用 url-loader。
yarn add url-loader -D
替换file-loader
module: { // 模块
rules: [ // 配置规则
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'url-loader',
options: {
limit: 5 * 1024, // 5k以下转成base64 以上转成真正的图片
}
}
}
]
}
ok,重新打包后,可以看到小图片被转成base64了
<img src="data:image/png;base64,/9j/4AAQSkZJRgABAQAASABIAAD"/>
但是这样会导致打包后的 js 文件变大,每次都需要重新打包,而在 webpack5 中,只要文件没有改变,就不会重新打包哦,这是 webpack5 性能提高 100% 的原因,就是靠硬盘缓存。
文件按类型打包到各自目录
图片分类 图片分类很简单,只需要把 url-loader 配置中加个 outputPath 即可,url-loader 会自动帮我们处理好图片的引入路径哦。
module: { // 模块
rules: [ // 配置规则
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'url-loader',
options: {
limit: 5 * 1024, // 5k以下转成base64 以上转成真正的图片
outputPath: 'img/'
}
}
}
]
}
css分类 css分类只需要更改 MiniCssExtractPlugin 输出的 filename 即可。
plugins: [ // 是一个数组 放着所有的webpack插件的实例
new MiniCssExtractPlugin({ // 抽离css
filename: 'css/main.css', // 抽离出的文件名
})
]
这时候 build 目录会出现 css/index.css 文件引入的背景图片路径为 img/xxx.png,解决办法是给 MiniCssExtractPlugin.loader 增加 publicPath
{
test: /\.less$/,
use: [
{ // 把css抽离出 并link标签引入
loader: MiniCssExtractPlugin.loader,
options:{
publicPath: '../'
}
},
'css-loader',
'postcss-loader',
'less-loader'
]
}
ok,齐活儿。
sourcemap
我们在解析 js 中,会把一些高级语法转换成低级语法,甚至生产环境伴随着代码,比如下面一行代码报错,我们很难追溯到报错的地方。
// index.js
class Log {
constructor() {
console.lo('报错了');
}
}
let log = new Log();
我们把 mode 改成 production,执行 npm run dev去启动服务。 页面报错了,错误出现在 bundle.js 第一行。
Uncaught TypeError: console.lo is not a function
at new t (bundle.js?03a176d0736bff01997d:1)
点进去一看,都是压缩过的代码(这里截取一部分),实际情况更复杂,更难调试。
log(s().next()),"sssss".includes("s");new function t(){o()(this,t),console.lo("报错了")}}.call(this,a(159))}
是不是应该有一个映射文件,我点进去看到的应该是源码。
module.exports = {
// 1) 源码映射 会单独生成一个 sourcemap文件 出错了会标识当前报错的列和行
devtool: 'source-map', // 增加映射文件 可以帮我们调试源代码
// 2) 'eval-source-map' 不会单独打包出 sourcemap 文件(集成到打包后的文件中),也可以显示行和列,并且显示源码
// devtool: 'eval-source-map',
// 3) 'cheap-module-source-map' 不会产生列,单独生成一个 sourcemap 文件
// devtool: 'eval-source-map',
// 4) 'cheap-module-eval-source-map' 不会产生列,不会产生文件,集成到打包后的文件中
// devtool: 'cheap-module-eval-source-map',
这时候打包,页面能看到报错列和行
index.js:28 Uncaught TypeError: console.lo is not a function
at new t (index.js:28)
点进去,可以看到源码
// ...
// 测试 source map
class Log {
constructor() {
console.lo('报错了');
}
}
// ...
关键字
这里多提一嘴,看似 source-map 的种类很多,其实只有五个关键字 eval,source-map,cheap,module 和 inline 任意组合,但是有顺序要求(以下基于 webpack5 测试)。
| 关键字 | 含义 |
|---|---|
| eval | 使用 eval 包裹模块代码(打包后的源码,通过 eval 包裹字符串执行,webpack5 中直接通过字符串比对,方便缓存) |
| source-map | 产生 .map 文件,包含行、列和 loader 的映射 |
| cheap | 不包含列信息也不包含 loader 的 sourcemap |
| module | 包含 loader 的 source(比如 jsx to js,babel 的 sourcemap),否则无法定义源文件 |
| inline | 将 .map 作为 DataURI 嵌入,不单独生成 .map 文件 |
怎么理解不包含 loader 的 soucemap 呢?
那么 cheap-module 跟 source-map 有什么区别呢,它们的区别在于,source-map 具有行和列的信息,而 cheap-module 不包含列,只有行的信息。
组合规则
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
- source-map 单独在外部生成完整的 sourcemap 文件,并且在目标文件里建立关联,能提示错误代码的准确原始位置
- inline-source-map 以 base64 格式内联在打包后的文件中,内联构建速度更快,也能提示错误代码的准确原始位置
- hidden-source-map 会在外部生成 sourcemap 文件,但是在目标文件里没有建立关联,不能提示错误代码的准确原始位置
- eval-source-map 会为每一个模块生成一个单独的 sourcemap 文件进行内联,并使用 eval 执行
- nosources-source-map 也会在外部生成 sourcemap 文件,能找到源始代码位置,但源代码内容为空
- cheap-source-map 外部生成 sourcemap 文件,不包含列和 loader 的 map
- cheap-module-source-map 外部生成 sourcemap 文件,不包含列的信息但包含 loader 的map
如何选 & 最佳实践
开发环境
- 我们在开发环境对sourceMap的要求是:速度快,调试更友好
- 要想速度快 推荐 eval-cheap-source-map
- 如果想调试更友好 cheap-module-source-map
- 折中的选择就是 eval-source-map
生产环境
- 首先排除内联,因为一方面我们了隐藏源代码,另一方面要减少文件体积
- 要想调试友好 sourcemap > cheap-source-map/cheap-module-source-map > hidden-source-map/nosources-sourcemap
- 要想速度快 优先选择 cheap
- 折中的选择就是 hidden-source-map
代码如何调试
测试代码如下
let a = 1;
let b = 2;
let c = 3;
debugger
console.log(a, b, c);
测试环境
我期望的肯定是,只有我自己能调试,别人调试不了,根目录下新建 maps 文件夹,用于存放打包后的 map 文件。
- 禁用 webpack 自动的 sourcemap 方案,手动维护
- 使用 filemanager-webpack-plugin 将打包后的 map 文件拆出来,并在源文件中插入映射标记代码(手动维护映射),映射地址是我本地的 .map 文件。
+ // 文件管理器插件
+ const FileManagerPlugin = require('filemanager-webpack-plugin');
module.exports = {
entry: {
main: './src/index.js',
},
mode: 'development',
+ devtool: false, // 不生成任何 sourceMap 信息,手动控制
plugins: [ // 是一个数组 放着所有的webpack插件的实例
new HtmlWebpackPlugin({
template: './src/index.html', // 模板路径
publicPath: './'
}),
+ new webpack.SourceMapDevToolPlugin({
+ filename: '[file].map' , // main -> main.js.map
+ // url 就是 main.js.map
+ append: `\n//# sourceMappingURL=http://127.0.0.1:3000/[url]`
+ }),
+ new FileManagerPlugin({
+ events: {
+ onEnd: {
+ copy: [
+ {
+ source: './dist/*.map', // 将 dist 目录下的所有 .map 文件
+ destination: path.resolve('maps') // 都拷贝到 maps 文件夹里
+ }
+ ],
+ delete: ['./dist/*.map'] // 处理完删除 dist/*.map
+ }
+ }
+ })
]
}
此时进行 npm run build,可以看到,在文件末尾指定了映射路径,并且 maps 也多出了 .map 文件~
/******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
var a = 1;
var b = 2;
var c = 3;
debugger;
console.log(a, b, c);
/******/ })()
;
//# sourceMappingURL=http://127.0.0.1:3000/main.js.map
这样就能实现,测试环境下,打包目录中没有 .map 文件,真正映射到的地址是我本地端口的文件,也就是只有我能调试啦~
生产环境
线上我们需要使用 hidden-source-map,它能实现打包出 map 文件,但是在源文件中不做关联,需要我们手动映射。
- const FileManagerPlugin = require('filemanager-webpack-plugin');
module.exports = {
entry: {
main: './src/index.js',
},
mode: 'development',
+ devtool: 'hidden-source-map', // 打包出 map 文件,但是在源文件中不做关联
plugins: [ // 是一个数组 放着所有的webpack插件的实例
new HtmlWebpackPlugin({
template: './src/index.html', // 模板路径
publicPath: './'
}),
- new webpack.SourceMapDevToolPlugin({
- filename: '[file].map' , // main -> main.js.map
- // url 就是 main.js.map
- append: `\n//# sourceMappingURL=http://127.0.0.1:3000/[url]`
- }),
- new FileManagerPlugin({
- events: {
- onEnd: {
- copy: [
- {
- source: './dist/*.map', // 将 dist 目录下的所有 .map 文件
- destination: path.resolve('maps') // 都拷贝到 maps 文件夹里
- }
- ],
- delete: ['./dist/*.map'] // 处理完删除 dist/*.map
- }
- }
- })
]
}
npm run build 中的 main.js 文件没有做关联~
/******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
var a = 1;
var b = 2;
var c = 3;
debugger;
console.log(a, b, c);
/******/ })()
;
启动服务,访问页面,我们入口文件加了 debugger 哦,发现没有定位到源文件,因为我们没有做关联,此时可以右键 add source map 我们本地打包出的 sourcemap 来做映射,同样达到本地调试线上代码的目的。