webpck 基础配置
安装依赖
npm init -yyarn add webpack webpack-cli webpack-dev-server
package.json 中配置
"scripts": {
"build": "webpack"
},
npm run build 执行webpack命令,先到项目的node_modules\bin\webpack.cmd执行,没有的话去当前设备中找webpack.cmd
webpack 打包配置
新建webpack.config.js文件
entry 单入口
entry: {
main: './src/index.js'
},
entry: './src/index.js',
entry: ['./src/index.js', './src/index1.js'],
entry: {
main: ['./src/index.js', './src/index1.js']
},
这4种entry的写法没有区别,数组也不是多入口的配置,最终都打包到一个main.js文件中,所以还是单入口的配置
entry 多入口
entry: {
index: './src/index.js',
index1: './src/index1.js'
},
output:{
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
entry 有多个key,output的filename 的[name]可以更具entry 的名字自动生成打包文件名称,才是多入口
loader
webpack 只能识别js,json文件,别的文件通过相应的loader将其转化成webpack可识别的文件
- 处理图片
webpack5之前处理图片要使用file-loader,url-loader,webpack5之后内置了资源,只需配置type就可以访问图片了
module: {
rules: [
// {
// test: /\.png$/,
// use: [{
// loader: 'url-loader',
// options: {
// name: '[hash:10].[ext]',
// esModule: false
// }
// }]
// }
{
test: /\.png$/,
type: 'asset/resource'
}
]
},
图片的引入不管是loader 还是 webpack5内置,都有一个问题就是不能在html中直接使用,如果使用需要别的loader 这里指的是src/images 下的图片不能直接在html 中使用
devServer: {
static: {
directory: path.join(__dirname, 'public'),
}, //让项目在浏览器,能访问静态文件
}
配置过这个的话,就可以在html 中直接使用bublic下面的图片,注意引用路径不要带public
<img src="/01.png" alt="" id="img1">
- 处理js babel-loader
npm i babel-loader @babel/core @babel/preset-env @babel/preset-react -D
npm install --save-dev @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators
@babel/preset-react react 需要,不是react 就不用安装
babel-loader 调用 @babel/core转化代码,@babel/preset-env告诉 @babel/core转化规则
- eslint
npm i eslint-loader eslint @babel/eslint-parser @babel/eslint-plugin -D
babel-eslint 被 @babel/eslint-parser 替换了
eslint-loader 弃用了,现在使用eslint-webpack-plugin 插件了 webpack.config.js
{
test: /\.jsx?$/,
enforce: 'pre', // 在同类文件中先执行
exclude: /node_modules/, // 排除文件和asset/resource互斥
use: [{
loader: 'eslint-loader',
options: { fix: true }, // 自动修复打包代码
}],
},
// 或者
{
plugins: [
new ESLintPlugin({
extensions: /\.jsx?$/,
exclude: 'node_modules',
fix: true,
}),
]
}
.eslintrc.js
module.exports = {
root: true,
parser: "@babel/eslint-parser",
parserOptions: {
requireConfigFile : false, // 解决项目中创建.eslintrc.js 文件之后第一行代码有红色波浪线的问题
sourceType: "module",
ecmaVersion: 2015
},
env: {
browser: true
},
plugins: [
"@babel/eslint-plugin"
],
rules: {
"indent": "off",
"no-console": "off"
}
};
每次手动配置很麻烦,eslint可以继承,使用社区的airbnb规则
npm i eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks -D
这几个插件之间是有依赖关系的,要统一安装
.eslintrc.js
module.exports = {
// root: true,
extends: 'airbnb',
parser: '@babel/eslint-parser', // 让eslint的语法也能支持babel的解析语法,是一个允许ESLint在由Babel转换的源代码上运行的解析器。
parserOptions: {
requireConfigFile: false, // 解决项目中创建.eslintrc.js 文件之后第一行代码有红色波浪线的问题
sourceType: 'module',
ecmaVersion: 2015,
},
env: {
browser: true, // 浏览器
node: true, // node
},
plugins: [
'@babel/eslint-plugin', // 辅助@babel/eslint-parser,能改变一些内置的规则来更好的支持实验特性
],
rules: {
indent: 'off',
'no-console': 'off',
},
};
配置文件中的rules可以覆盖airbnb规则
plugin
扩展webpack的功能
- html-webpack-plugin 在html模版中自动引入打包后的js文件
plugins:[
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
})
]
- clean-webpack-plugin 打包之前清理目标文件目录
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['**/*'],
}),
]
}
- copy-webpack-plugin copy 文件到指定目录
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
plugins: [
new CopyWebpackPlugin({
patterns: [{
from: path.resolve(__dirname, 'src/copy'),
to: path.resolve(__dirname, 'dist/copy'),
}],
}),
]
}
- mini-css-extract-plugin 生产环境提取css文件
mode
- development 开启debug工具,打印错误信息,编译速速会快 process.env.NODE_ENV = development
- production 开启优化,打包结果优化,webpack性能优化 process.env.NODE_ENV = production
- mode 的默认值是production
设置环境变量
- --mode
--mode的方式
process.env.NODE_ENV在模块文件中可以访问到结果,在webpack.confing.js这样的配置文件中不能访问到结果yarn add webpack-dev-server
"scripts": {
"build": "webpack --mode=production",
"start": "webpack serve --mode=development"
}
npm start 运行程序,浏览器中打开
- --env webpack.config.js配置文件以函数接收的方式,能在wenpack配置文件中获取环境变量信息
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = (env) => {
console.log('----------------env', env, process.env.NODE_ENV)
// ----------------env { WEBPACK_SERVE: true, development: true } undefined
return {
mode: 'development',
entry: {
main: './src/index.js'
},
output:{
path: path.resolve(__dirname, 'dist'),
// filename: '[name].js'
filename: 'main.js'
},
plugins:[
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
})
]
}
}
"scripts": {
"build": "webpack --env=production",
"start": "webpack serve --env=development"
}
build 之后模块中的process.env.NODE_ENV不会随着--env改变,依旧是development
- cross-env 修复差异,让环境变量在模块和配置文件中都可以得到
"scripts": {
"build": "cross-env NODE_ENV=production webpack",
"start": "cross-env NODE_ENV=development webpack serve"
},
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const Webpack = require('webpack')
module.exports = (env) => {
console.log('------------CONFIG----process.env.NODE_ENV', process.env.NODE_ENV)
// const isDevelopment = env.development
return {
mode: process.env.NODE_ENV,
entry: {
main: './src/index.js'
},
output:{
path: path.resolve(__dirname, 'dist'),
// filename: '[name].js'
filename: 'main.js'
},
plugins:[
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
}),
new Webpack.DefinePlugin({
// process.node_env只是一个常量,可以是任何名词
'process.node_env': JSON.stringify(process.env.NODE_ENV)
})
]
}
}
在index.js 文件中
console.log('-------------------INDEX-----process.env.NODE_ENV-----', process.env.NODE_ENV);
console.log('-----------------INDEX--process.node_env----------', process.node_env);
在webpack.config.js 中
console.log('------------CONFIG----process.env.NODE_ENV', process.env.NODE_ENV)
都能获取到对应的环境变量
- DefinePlugin
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const Webpack = require('webpack')
module.exports = (env) => {
console.log('----------------env', env, process.env.NODE_ENV)
console.log('-----------------webpack--process.node_env----------', process.node_env);
// ----------------env { WEBPACK_SERVE: true, development: true } undefined
// -----------------webpack--process.node_env---------- undefined
const isDevelopment = env.development
return {
mode: 'development',
plugins:[
new Webpack.DefinePlugin({
'process.node_env': JSON.stringify(isDevelopment ? 'development' : 'production')
})
]
}
}
配置文件获取不到,但是模块文件可以获取到'process.node_env 的值
总结:配置文件中的mode 设定的值,才是在模块文件中真的生效的环境值,但是为了能灵活的在配置文件中配置环境,所以使用cross-env 插件。
devServer
devServer: {
static: {
directory: path.join(__dirname, 'public'),
}, //让项目在浏览器,能访问静态文件
hot: true, // 热更新
port: 3200,
open: false, // 是否自动打开浏览器
compress: true, // 启动gzip压缩
}
http://localhost:3200/01.png 可以访问到public目录下的图片
devtool
- 开发
devtool: 'eval-source-map'这样配置就够了 - 生产
devtool: 'eval-source-map'生产环境中最好不要显示出map文件,方式代码泄漏,要是调试,可以手动的在浏览器中配置map做映射
externals
可以引入外部文件,这样就可以减小打包文件的体积 详情
hash,chunckHash,contentHash
- hash 每次打包都会产生hash,作为文件的标识,防止文件打包之后重复文件名,只要项目里面的某一个文件发生变化,所有的文件hash都要重新生成
- chunkhash 打包文件之间有相互依赖的关系,index1.js 依赖index2.js 这样的关系就是chunk,webpack打包时,根据chunk产生的hash 就是chunkHash,chunk 发生改变,只有这个模块的hash会发生变化,别的没有依赖的模块不变化
- contenthash 根据内容产生的hash,内容不变hash不变。比如: index1.js 里依赖 01.css,打包后会产生index1.[contenthash].js 和 01.[contenthash].css, index1.js 里面的内容变化了,01.css 没做改变,再次打包之后只有index1.js 的hash值会变化
hash 也可以写作 fullhash
hash值越精确,打包越慢,contenthash最慢
css 兼容
npm i postcss-loader postcss-preset-env -D
webpack.config.js
{
test: /\.css$/,
exclude: /node_modules/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
postcss.config.js
module.exports = {
plugins: [
[
'postcss-preset-env',
{
// Options
browsers: 'last 5 versions',
},
],
],
};
html css js 压缩
html5 中mode=production的时候,会自动压缩
px 转化rem
npm i px2rem-loader lib-flexible -D
在全局共用模块中import 'lib-flexible'
webpack.config.js
{
test: /\.css$/,
exclude: /node_modules/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
{
loader: 'px2rem-loader',
// options here
options: {
remUni: 75,
remPrecision: 8,
},
},
],
},
polyfill
babel在转化js的时候,只能转化当前进度是stage4的语法,小于4的不能转化,而有的浏览器平台,不能支持我们,在代码中所写的新的语法和api,所以需要polyfill帮助我们抹平平台差异
@babel/polyfill
npm install --save @babel/polyfill
browsers
.browserslistrc
last 50 version
> 1%
not dead
polyfill 的抹平依据browserslistrc的指定规则,如果不配置,打包会报错
@babel/preset-env
babel.config.json
{
"presets": ["@babel/preset-env"], // 从前向后执行
"plugins": []
}
polyfill 当前依据browserlistrc和babel.config.json中的@babel/preset-env可以在打包的时候处理js中新的语法问题。如() => {}箭头函数,但是class, Promise这样的新api还是不能处理
为了处理这种不同平台的差异可以在"@babel/preset-env中添加useBuiltIns 选项
- useBuiltIns: false
不管.browserslistrc 配置的规则把所有的抹平文件全部引入,文件的体积会很大
并且需要手动在入口文件import '@babel/polyfill';
- useBuiltIns: entry "corejs": 2
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": 2
}
],
], // 从前向后执行
会根据浏览器的差异,加入补丁,文件体积依据配置的打包规则而定,但是还是需要手动在入口文件
import '@babel/polyfill';
"corejs": 3 (当前最新的用法)
npm i core-js@3 regenerator-runtime --save
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": 3
}
],
]
需要手动在入口文件
import 'core-js/stable';
import 'regenerator-runtime/runtime';
- useBuiltIns: usage
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
],
]
会根据浏览器的差异,加入补丁,文件体积会明显变小,且不用手动引入@babel/polyfill或者import 'core-js/stable';import 'regenerator-runtime/runtime';
entry 在入口文件处引入就会挂载到全局变量上,usage会在打包的时候,在每一个有语法抹平的模块中增加抹平代码的代码块,类似
reqire('XXX')
适合项目中使用
@babel/plugin-transform-runtime 解决全局污染
"@babel/preset-env"下useBuiltIns的usage会每次重复引入依赖,entry会污染全局环境,且2种方式对应的corejs还需要手动安装包。使用@babel/plugin-transform-runtime插件可以自动导入所需依赖,减少开发者的配置,不污染全局变量,且效果和usage十分相似。
npm i @babel/plugin-transform-runtime --save
babel.config.json
{
"presets": [
[
"@babel/preset-env"
// 可以被下面的"@babel/plugin-transform-runtime"配置替代
// {
// "useBuiltIns": "entry",
// "corejs": 3
// }
],
"@babel/preset-react"], // 从前向后执行
"plugins": [ // 从后向前执行
[
"@babel/plugin-transform-runtime",
{
"corejs": 3,
"helpers": true,
"regenerator": true
}
],
]
}
适合组件库或者插件里使用
webpack 高级进阶
AST语法树
javascript parser 可以把代码转化为ast语法树,ast语法树定义了代码结构,通过这颗树,可以精准定位到声明语句,赋值语句,运算语句,可以对代码,分析,优化,变更。
yarn add esprima estraverse escodegen -D
- esprima 把代码解析成ast
- estraverse 遍历语法树
- escodegen 把ast树再转换成源代码
ast的语法遍历是深度遍历,并且会跳过没有type的节点
function ast(){}
babel 转化箭头函数
const core = require('@babel/core')
const types = require('babel-types')
let arrowFunction = require('babel-plugin-transform-es2015-arrow-functions')
let codeEs6 = `
const sum = (a, b) => {
return a+b
}
`
let arrowFunctionBySelf = {
visitor: {
ArrowFunctionExpression(nodePath) {
let node = nodePath.node
node.type = 'FunctionExpression'
}
}
}
let codeEs5 = core.transform(codeEs6, {
plugins: [arrowFunctionBySelf]
})
// 箭头函数的最终转化结果
console.log('-----------------------------', codeEs5.code);
- 箭头函数的ast
- 非箭头函数的ast
可以看到箭头函数和普通函数的差别是init时候的node的type类型,在遇到箭头函数ArrowFunctionExpression时把node的type转化成普通函数的类型FunctionExpression,就可以了。
- class 转化 构造函数
const core = require('@babel/core')
const t = require('babel-types')
const codeEs6 = `
class Person {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
`
const classPlugin = {
visitor: {
ClassDeclaration(nodePath) {
const {
node
} = nodePath
const id = node.id
// constructor的kind和getName的kind 是不一样的
const methods = node.body.body
let nodes = []
methods.forEach((item) => {
if (item.kind === 'constructor') {
// 创建constructor ast树转化之后的函数
const constructorFun = t.functionDeclaration(id, item.params, item.body, item.generator, item.async);
nodes.push(constructorFun)
} else {
// 转化getName函数转化之后的函数
const memberExpressionPrototype = t.memberExpression(id, t.identifier('prototype'));
// 左边
const leftMember = t.memberExpression(memberExpressionPrototype, item.key);
// 成员函数(右边)
const functionExpression = t.functionExpression(id, item.params, item.body, item.generator, item.async);
// 整个函数
const assignmentExpression = t.assignmentExpression('=', leftMember, functionExpression);
nodes.push(assignmentExpression)
}
})
if (nodes.length === 1) {
// 只有构造函数
nodePath.replaceWith(nodes[0])
} else {
nodePath.replaceWithMultiple(nodes)
}
}
}
}
const codeEs5 = core.transform(codeEs6, {
plugins: [classPlugin]
})
console.log('-------------codeEs5----------------', codeEs5.code);
webpack 工作流程
- 获取配置文件和shell(npm run XXX --params)参数合并
- 依据参数产生compiler编译对象
- 加载所有的配置插件
- 执行compiler的run方法
- 根据配置中的entry找出入口文件
- 从入口文件调用所有的loader对模块进行编译
- 找到所有本地模块依赖的模块,直到所有入口依赖的模块都处理过
- 根据入口和模块之间的依赖关系,组成包含多个模块的chunk
- 把chunk转换成文件输出
webpack.js
let Compiler = require('./Compiler')
function webpack(options) {
// 初始化配置参数
const shellOPtions = process.argv.slice(2).reduce((config, args) => {
let [key, value] = args.split('=')
config[key.slice(2)] = value
return config
}, {})
let optionsAll = {
...shellOPtions,
options
}
const compiler = new Compiler(optionsAll)
let plugins = options.plugins || []
if (plugins && Array.isArray(plugins)) {
plugins.forEach((plugin) => {
plugin.apply(compiler)
})
}
return compiler
}
module.exports = webpack;
complier.js
class Compiler {
constructor(options) {
// console.log('-----------------------------', options);
}
run() {
}
}
module.exports = Compiler
const webpack = require('./webpack');
const options = require('./webpack.config')
let compiler = webpack(options)
webpack 热加载流程
- 创建
compiler编译对象 - 利用HotModuleReplacementPlugin插件生成对应的hash结果文件
- 创建server服务器,为entry添加别的客户端和服务端文件
- 创建express实例,接受创建的中间件,通过compiler的watch监听文件的变化,每次文件变化,就重新编译,并且把编译结果放在内存里面
- 创建
websocket服务器,告诉客户端变化之后产生的hash和ok,主要通过webpack-dev-server 调用 webpack api 监听 compile的done事件
webpackHmr/node_modules/webpack-dev-server/lib/Server.js
setupHooks() {
const addHooks = (compiler) => {
compiler.hooks.invalid.tap("webpack-dev-server", () => {
if (this.webSocketServer) {
this.sendMessage(this.webSocketServer.clients, "invalid");
}
});
compiler.hooks.done.tap("webpack-dev-server", (stats) => {
if (this.webSocketServer) {
this.sendStats(this.webSocketServer.clients, this.getStats(stats));
}
this.stats = stats;
});
};
if (this.compiler.compilers) {
this.compiler.compilers.forEach(addHooks);
} else {
addHooks(this.compiler);
}
}
- 客户端接收到服务端发出的消息,根据type类型作出响应,type是hash就把hash存起来,是ok就执行reload操作
- 客户端根据hash向服务端请求是不是要更新
- 服务端接收到请求,更新代码 详情
loader
loader本质上是一个函数,接收资源,为资源代码处理,返回处理结果,下一个loader接着处理上一个loader处理之后的结果。
loader 有4种 pre normal inline post 通过enfore配置。
loader 的内部有一个pitch的属性函数,loader真正执行的时候,是按照先从前向后运行pitch 函数,没有pitch 或者pitch没有返回值,就处理下一个loader的pitch,当loader的pitch处理完成之后,在从后向前处理loader的函数实体。如果loader的pitch有返回值,就从当前loader开始向前处理loader实体。
loader 中相关的api在loader-runner
简单的file-loader
loader-utils中有获取loader参数的api
const {
getOptions,
interpolateName
} = require('loader-utils')
function loader(content) {
let options = getOptions(this) || {}
// hash
let filename = interpolateName(this, options.filename, {
content
})
this.emitFile(filename, content)
return `module.exports=${JSON.stringify(filename)}`
}
loader.raw = true
module.exports = loader;
plugin
webpack 的插件把自己的方法,放在webpack 的钩子上,在webapck的编译过程中,触发自己的插件方法
插件是一个类或者构造函数,在原型上有一个apply方法
tapable包是实现插件的主要工具
webpack配置优化
- estensions
resolve: {
extensions: ['.js', '.json'],
},
- 配置别名加快查找速度,不需要从node_modules中按模块规则查找
resolve: {
alias: {
Utilities: path.resolve(__dirname, 'src/utilities/'),
Templates: path.resolve(__dirname, 'src/templates/'),
},
}
- modules 默认node_modules,加上配置之后,会先查找配置的文件,找到之后,就不继续向下查找了
resolve: {
modules: [path.resolve(__dirname, 'xxx'), 'node_modules'],
}
- noParse 对于没有使用import,require,define的大型包,可以省略解析步骤
module: {
noParse: (content) => /jquery|lodash/.test(content),
},
- webpack.IgnorePlugin 去除包里面无用的内容,减小包的体积
new webpack.IgnorePlugin({ resourceRegExp: /xxx/, contextRegExp: [moduleName] });
- speed-measure-webpack-plugin 打包速度分析插件
- webpack-bundle-analyzer 生成代码分析报告插件
- purgecss-webpack-plugin 可以去除未使用的css,必须和mini-css-extract-plugin一起使用
- thread-loader 放置在这个loader之后的loader会在单独的worker池中运行,但是每个worker都是一个独立的node.js进程,它有大约600ms的开销。还有进程间通信的开销。 只在昂贵的操作中使用这个加载器!
import和commonjs在打包过程中的不同点
- commonjs调用commonjs: commonjs模块原样输出
- commonjs调用es6模块: 会在exports上添加__esModule:true
- es6调用es6: 未被使用的模块会在压缩的时候去除
- es6调用commonjs: commonjs模块不会被webpack编译,commonjs模块没有default属性
dev-server如何启动的
- 启动一个http服务
- webpack构建的时候输出bundle到内存,http服务器从内存中读取bundle文件
- 监听文件变化,之后重新执行第二步
webpack数据持久化缓存实现
- 服务服端设置缓存头
- 打包依赖和运行时到不同的chunk中(splitChunk)
- 延迟加载,使用import()的方式可以动态加载到文件分到独立的chunk,得到chunkHash
- 保证hash值稳定,编译的文件内容的hash,尽量不影响其他的文件的hash
webpack遇到import会发生什么
import的设计思想是,在遇到import不立即执行,还是产生一个引用,等到需要使用的时候,再去模块里面取值
webpack在遇到import的时候,会把import转化成commonjs,然后将node_module里面的依赖打包成自执行函数
软件优化
- html优化,减少空格,减少table,不用iframe
- css优化,能使用css的尽量不使用js,用transform替代position,left,top会重绘和重排
- js图片优化,懒加载,延迟加载。视频音频在使用的时候才加载,不要使用闭包
- http:尽量减少
- 利用浏览器的缓存,把不常更新的静态资源做缓存处理(304)