-
一般来说
-
index.html 放在自己的服务器上 不开启缓存 方便更新
-
index.html 引用的静态文件 js css 要加 hash 值 存放在 cdn 上 进行长期缓存
-
tree-shaking
import {join} from 'lodash'会打包整个 lodashimport join from 'lodash/join'只打包 join 模块 如果就想import {join} from 'lodash'这样引入,又想实现按需加载,需要使用 babel-plugin-import
loader
- loader 是有分类的
- pre 前置
- normal 正常
- inline 内联
- post 后置
- pre => normal => inline => post
关于模式 mode
development 与 prodution
- 设置 process.env.NODE_ENV 的值
- development 模式 -> development
- production 模式 -> production
区分环境
- 四种方式
- --mode 用来设置模块内的 process.env.NODE_ENV
- --env 用来设置 webpack 配置文件的函数参数
- cross-env 用来设置 node 环境的 process.env.NODE_ENV
- DefinePlugin 用来设置模块内的全局变量
- 只有 mode 会影响 webpack 插件的启用, env 不会
- --mode: 只能在模块内部拿到环境变量
- 可以在模块内通过
process.env.NODE_ENV获取当前环境变量, 无法在 webpack 配置文件中获取此变量 - 在 webpack 配置文件导出对象中配置 mode
- 可以在模块内部拿到
- 但是命令行里的配置优先级比较高
- 可以在模块内通过
"script": {
"build": "webpack --mode=production",
"start": "webpack serve --mode=development"
}
// webpack 5 中执行webpack serve 会默认使用webpack-dev-server服务
-
--env: 可以在 webpack 配置文件中拿到环境变量
- 这样配置是给 webpack 配置文件了
- 这种配置在模块内部拿不到环境变量,在 webpack 配置文件中也拿不到环境变量
- 但是当 webpack 配置文件导出的是一个函数的时候,可以在参数中拿到环境变量
"script": {
"build": "webpack --env=production",
"start": "webpack serve --env=development"
}
// webpacck.config.js
module.exports = (env, argv) => {
return {
entry,
output,
module,
plugins
}
}
-
DefinePlugin 定义全局变量的插件
- 设置全局变量(不是 window)所有模块都能读取到该变量的值
- 可以在任意模块内通过
process.env.NODE_ENV获取当前的环境变量 - 但是无法在 node 环境(webpack 配置文件中)下获取当前的环境变量
- 因为--mode 只能在模块内,--env 只能在 webpack 配置的函数参数中,都只能满足其中一个方面
- 于是利用 DefinePlugin,在--env 的前提下,使环境变量能够在各个模块中也能获取到
- 在 DefinePlugin 中设置值时加上 JSON.stringify 是因为如果不加,编译之后会是一个变量, 是为了保证编译之后值是一个字符串 编译之后可以在模块中拿到对应的值,相当于一个常量
"script": {
"build": "webpack --env=production",
"start": "webpack serve --env=development"
}
// webpacck.config.js
module.exports = (env, argv) => {
let isDevelopment = env.development; //是否是开发环境
let isProduction = env.production; //是否是生产环境
return {
entry,
output,
module,
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(isDevelopment? 'development':'production')
})
]
}
}
| 配置方式 | index.js 模块 | webpack.config.js |
|---|---|---|
| package.json 中配置 mode | 可以 | 不可以 |
| package.json 中配置 env | 不可以 | 不可以 |
| DefinePlugin | 可以 | 不可以 |
| cross-env | 不行,但是有办法 | 可以 |
- cross-env
- npm i cross-env -D
- 因为 Windows/mac/linux 设置环境变量的方式不一样
- window: set key=value
- mac: export key=value
- 为了跨平台 使用 cross-env
- cross-env 是跨平台设置环境变量的意思
- 配置之后可以在 webpack 配置文件中使用
- 可以通过 DefinePlugin 将值传递到模块中使用
"script": {
"build": "cross-env NODE_ENV=production webpack",
"start": "cross-env NODE_ENV=producction webpack serve"
}
// webpack.config.js
console.log('process.env.NODE_ENV', process.env.NODE_ENV);
module.exports = {
mode: process.env.NODE_ENV
entry,
output,
module,
plugins: [
new webpack.DefinePlugin({
'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
})
]
}
// index.js
// mode设置的,defineplugin传递的都可以使用 都是cross-env设定的值
console.log('mode设置的', process.env.NODE_ENV);
console.log('definePlugin设置的 NODE_ENV', NODE_ENV)
dotenv
- 使用 dotenv,只需要将程序的环境变量配置写在.env 文件中
- npm i dotenv-expand -D
// 使用
// 创建.env文件
MONGODB_HOST=localhost
MONGODB_PORT=27017
MONGODB_DB=test
MONGODB_URI=mongodb://${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_DB}
// useEnv.js
const dotenvFile = '.env'
require('dotenv-expand')(
require('dotenv').config({
path: dotenvFile,
})
);
console.log(process.env.MONGODB_HOST);
console.log(process.env.MONGODB_PORT);
console.log(process.env.MONGODB_DB);
console.log(process.env.MONGODB_URI);
开发环境配置
开发服务器
- 安装: npm i webpack-dev-server -D
- 在 webpack 配置文件的配置对象中: 配置
devServer这是一个选项,用来配置开发服务器 - 配置解析
- contentBase
contentBase: path.resolve(__dirname, "dist")意思是将 dist 目录作为静态服务的根目录,但是实际上这句话没有什么意义,因为打包会生成一个 dist 目录,在访问开发服务器的时候,默认就可以访问 dist 目录下打包出来的文件(无论打包出来的文件夹配置成什么名字都可以访问到),也就是说打包生成的文件夹不需要contentBase配置默认就是一个静态服务根目录- contentBase 选项的真实含义是配置额外的静态文件根目录,不用配置打包生成的文件目录(dist 目录)=> 如增加一个放置了图片等静态资源的文件夹 public,就需要配置才能访问
contentBase: path.resolve(__dirname, "public")- 所以 contentBase 是开启一个新的静态服务根目录的意思, dist 目录默认就能用
- 如果配置之后 dist 下有 main.js 文件,public 下也有 main.js 文件,访问时会是 dist/main.js,会先读取打包的静态文件夹
- compress: true, 启用压缩 gzip
- port: 8080 端口号
- open: true, 启动之后自动打开浏览器
- devServer 中的 publicPath:表示打包生成的静态文件所在的位置(若是 devServer 里面的 publicPath 没有设置,则会认为是 output 里面设置的 publicPath 的值) output 中的 publicPath 表示的是打包生成的 index.html 文件里饮用资源的前缀
- contentBase
// webpack.config.js
module.exports = {
mode,
entry,
output,
module,
plugins,
devServer: {
contentBase: path.resolve(__dirname, 'public'),
compress: true, // 启用压缩
port: 8080,
open: true, // 启动之后自动打开浏览器
},
};
支持 css
- css-loader 用来翻译处理@import 和 url()
- style-loader 把 css 插入 dom
- 最后执行的 loader(也就是最左边的 loader)一定要返回一个 js 脚本
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
};
- css-loader 和 style-loader 的区别
css-loader 用来处理@import 引入一个 css 或者 url()里放背景图 style-loader css 转成 js 写一段脚本插入页面
- css-loader 的
importLoaders参数
{
test: /\.css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: { // 如果不配置,默认值是0
importLoaders: 1, //对于包含的css文件而言,要使用前面的几个loader来处理
// 这里没有less-loader那样强大的处理,对于@import需要重头开始走postcss-loader处理,所以这里importLoaders设置为1
}
},
"postcss-loader"
]
}
{
test: /\.less$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
importLoaders: 0, // 引入的文件不需要再重头走less-loader,postcss-loader,因为less-loader很强大,直接会处理@import,到css-loader的时候已经没有@import了,所以importLoaders不需要额外配置
}
},
"postcss-loader",
"less-loader"
]
}
- less-loader 会将@import 处理成好, 不需要 css-loader 对@import 做处理了
支持 less 和 sass
- 安装 npm i less less-loader node-sass sass-loader -D
- less 里面可以@import css 或者 less 文件,但是不能和 sass 混用
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
},
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
],
},
};
支持图片
- npm i file-loader url-loader html-loader -D
- file-loader 解决 css 等文件中的引入图片路径问题
- url-loader 当图片小于 limit 的时候会把图片 base64 编码,大于 limit 参数的时候还是使用 file-loader 进行拷贝
- 使用图片的几种方式
let imageSrc = require('./images/1.jpg'); let img = new Image(); img.src=imageSrc; document.body.appendChild(img);background-image: url('./images/1.jpg')<img src='./images/1.jpg'/>处理不了 需要 html-loader 来处 理 在 html 里直接通过相对路径的方式引入(这种方式严重不推荐),可以直接引入 cdn 地址或者引入设置为静态文件目录的 public 下面的文件
module.exports = {
module: {
rules: [
{
test: /\.(jpg|png|bmp|gif|svg)$/,
use: [
{
loader: 'url-loader',
options: {
name: '[hash:10].[ext]',
esModule: false, //是否包装成一个es6模块(包装需要这样取值: module.default)
limit: 8 * 1024, //8K
},
},
],
},
{
test: /\.html$/,
use: ['html-loader'], //这样就可以在html中使用图片资源文件
},
],
},
};
js 兼容性处理
- Babel 其实是一个编译 javascript 的平台,可以把 es6/es7/react 的 jsx 转义成 es5
- 安装依赖 npm i babel-loader @babel/core @babel/preset-env @babel/preset-react -D npm i @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties -D
- babel-loader 使用 babel 和 webpack 转译 javascript 文件,调用的是@babel/core 这个核心包里的文件
- @babel/core babel 编译的核心包
- @babel-preset-env
这三者的关系 webpack loader 的本质就是一个函数,接受原来的内容返回新的内容,所以 babel-loader 就是一个函数,他的功能就是得到源代码,输出处理后的新代码,就这些
function babelLoader(source){ return targetSource}从源代码到转换后的代码(高级语法=>es5)的转换靠的就是@babel/corelet targetSource = babelCore.transform(source)是一个转换代码的引擎,只是个引擎,实际上并不知道要怎么转,默认情况下(在不做任何配置的情况下)你给他输入什么就会输出什么 真正的转换依靠 @babel/preset-env 是具体的转换规则 (es6 => es5 的规则)
function babelLoader(source) {
let targetSource = babelCore.transform(source, {
presets: ['@babel/preset-env'],
});
}
- @babel/preset-react babel 预设 将 react => es5
- @babel/plugin-proposal-decorators 将类和对象装饰器编译成 es5
- @babel/plugin-proposal-class-properties 将类的属性编译成 es5
- 1. @babel/plugin-proposal-decorators 和 @babel/plugin-proposal-class-properties 要一起使用; 2. 并且顺序要 decorators 在前 class-properties 在后 3.legacy 配置为 true 的时候 loose 也要为 true
- 插件是从后往前执行,预设是从前往后执行
// 预设:插件包。es6的语法规则有很多,对应就有很多的插件,把这些插件打成一个包,就被称为一个预设
// 插件:是一个一个的转换规则
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
options: {
presets: ["@bable/preset-env"],
plugins: [
["@babel/plugin-proposal-decorators", {legacy: true}],
["@babel/plugin-proposal-class-properties", {loose:true}]
]
}
}
}
]
}
}
// ["@babel/plugin-proposal-decorators", {legacy: true}]
// jsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true
}
}
// target 装饰的目标 key 属性名 属性描述器 decorator
function readonly(target, key, descriptor) {
descriptor.writable=false;
}
class Person{
@readonly PI=3.14
}
let p = new Person();
p.PI = 3.15
console.log(p); //PI不会被更改
// legacy: true 是转译装饰器老的写法 ==> 放在类的左边
// @classDecorator
// class P{}
// 新的写法是 ==> 放在类和名的中间: class @classDecorator P{}
// ["@babel/plugin-proposal-class-properties", {loose:true}]
class Person{ name="haha" }
let p = new Person();
// loose = true 会转义成 p.name = "haha"
// loose = false 会转义成 Object.defineProperty(p, 'name', { value: "haha" })
Eslint 代码校验
- 安装: npm i eslint eslint-loader babel-eslint -D
- 要想 eslint-loader 工作,要添加配置文件.eslintrc.js
- vscode 安装 eslint 在根目录创建.vscode/settings.json
// .vscode/settings.json
{
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.codeActionsOnSave":{
"source.fixAll.eslint": true
}
}
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'eslint-loader',
options: { fix: true },
exclude: /node_modules/,
enforce: 'pre',
},
],
},
};
// .eslintrc.js
module.exports = {
// root: true, //表示这是一个根配置文件,表示从零开始自己编写,还可以继承别人写好的,使用extends
extends: 'airbnb',
parser: 'babel-eslint', // 因为想进行代码检查,先要将代码转成抽象语法树,就是靠babel-eslint来转(解析器)
parserOptions: {
sourceType: 'module', //源代码类型
ecmaVersion: 2015,
},
env: {
browser: true, //代码会在浏览器运行
node: true,
},
rules: {
indent: 'off', //关闭锁进检查
quotes: 'off', //关闭引号类型检查
'no-console': 'error',
},
};
sourceMap
-
sourceMap 是为了解决开发代码和实际运行代码不一致时帮助我们 debug 到原始开发代码的技术,把源代码和编译文件对应起来的技术
-
看似配置想很多,其实就是五个关键字的任意组合
eval、source-map、cheap、module、inline -
关键字可以任意组合,但是有顺序要求
eval 使用 eval 包裹模块代码 source-map 产生.map 文件 cheap 不包含列信息,也不包含 loader 的 sourcemap module 包含 loader 的 sourcemap(比如 jsx to js,babel 的 sourcemap)否则无法定义源文件 inline 将.map 作为 DataURI 嵌入,不单独产生.map 文件
-
打包后的代码都是一行,源代码所有行的错误都对应打包后代码的第一行,没有意义。所以
cheap只用于开发环境,因为开发环境不进行压缩,不会把所有代码变成一行 -
webpack 要将 react 源文件打包成 es5 代码,由于 webpack 不识别 jsx 代码,无法直接转换,所以先通过 babel-loader 将 jsx 转成 es5 的 js 代码,再通过 webpack 将其转换成打包后的代码 react 源文件 => es5 js 代码 => webpack 打包后代码 反过来生成 sourcemap 只能对应到 es5 js 代码,无法对应到真实的源代码,调试肯定是希望看到源代码而不是 通过 babel 转译之后的 es5 代码 可以在 react 通过 babel 编译的时候也生成一个 sourcemap 给 webpack,webpack 结合 sourcemap 和 es5 js 代码计算出最终的打包代码又生成一个 sourcemap,这样最终的 suorcemap 就包含之前的 sourcemap,就可以对应到源代码了,这就是
module关键字的作用 -
组合规则
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-mapinline-source-map 包含完整的行和列 以 base64 格式内联在打包后的文件里
-
source-map 最完整也是最慢的 包含完整的行和列
- 生成单独的 source-map 文件
- 包含完整的行和列信息
- 在目标文件里建立关系,从而能提示源文件原始位置
-
inline-source-map
- 以 base64 格式内联在打包后的文件里
- 包含完整的行和列的信息
- 在目标文件里建立关系,从而能提示源文件原始位置
- 不会生成单独的文件
- 包含 module
-
hidden-source-map
- 会在外部生成 source-map,但是在目标文件并没有建立关联,也不能提示原始错误位置
- 上线的代码中不能有 source-map,有可能会泄漏代码
- 但是在线上出 bug 的时候,需要调试,需要源代码
-
eval-source-map
- 会为每一个模块生成一个单独的 sourcemap 进行关联,并使用 eval 执行
- 包含 module
- eval 的内联是不一样的,eval 每个模块单独生成 map 单独缓存;inline 还是放在一起,只是会变成 base64 字符串内联到打包后的文件
-
nosources-source-map
- 会在外部生成 sourcemap 文件,也能找到源代码的位置,但是源代码的位置是空的
- 没有源代码,不能调试
- 一方面可以帮助定位错误的位置,另一方面又不会泄漏代码
-
cheap-source-map
- 轻量级的便宜的
- 只包含行映射,不包含列映射
- 不包含 babel 的 map 映射,也就是只能定位到通过 babel 转换成的 es5 js 代码,不能对应上源代码
- 不能看到最原始的代码
- 只有 cheap 不包含 module,其他的都包含
-
cheap-module-source-map
- 为了在轻量级的基础上对应到源代码,加上 module
- 如果是 source-map 前面不用加 module,因为他本身就已经是最全的了
- 只包含行不包含列
- 包含 babel 的 map 映射,可定位到最原始的代码
-
最佳实践
-
开发环境
开发环境对 sourcemap 的要求是:速度快 调试更友好 要想速度快 推荐 eval-cheap-source-map (eval 重新编译的时候效率更高) 想调试友好 cheap-module-source-map 折中的选择是 eval-source-map
-
生产环境
首先排除内联; 因为一方面要隐藏源代码,另一方面要减少文件体积 想调试友好 source-map > cheap-source-map/cheap-module-source-map > hidden-source-map > nosources-source-map 想速度快,优先选择 cheap 折中的选择是 hidden-source-map
-
为什么 eval 包裹?为什么更快?
"source-map" 不能缓存模块的 sourcemap,每次都要生成完整的 sourcemap,把所有的 map 存放成一个文件,例如 100 个模块,只要改一个,缓存就会失效,整个 map 都需要重建 "eval-source-map"和 source-map 的内容一样多,但是可以缓存每一个模块的 sourcemap,在重新构建的时候速度更快
-
测试环境
source-map-dev-tool-plugin 实现了对 source map 生成进行更细粒度的控制 filename(string): 定义生成的 source map 的名称(如果没有值将会变成 inlined) append(string): 在原始资源后追加给定值。通常是#sourceMappingURL 注释。[url]被替换成 source map 文件的 URL
// 1. 控制台settings-preferences-sources中启用: enable javascript source maps
// 2. 让那个部署后的脚本指向自己的source map服务器
// webpack.config.js
const FileManagerPlugin = require('filemanager-webpack-plugin');
export default {
devtool: false, // 把devtool关闭掉
entry,
output,
module,
plugins: [
// 不再让webpack帮我们生成sourcemap
new webpack.SourceMapDevToolPlugin({
// 会在打包后文件的尾部添加一行这样的代码
append: `\n//# sourceMappingURL=http://127.0.0.1:8081/[url]`,
filename: '[file].map', // 如main.js.map
}),
// 文件管理插件,可以帮我们拷贝代码 删除代码
// 先将生成的map文件拷贝到指定的目录下,然后删除dist下的map文件
// 只有本人可以调试,因为map文件存放在本机,别人拿不到,部署只会部署dist下的文件
new FileManagerPlugin({
events: {
onEnd: {
copy: [
{
source: './dist/*.map',
destination: 'C:/aproject/zhufengwebpack202103/maps',
},
],
delete: ['./dist/*.map'],
},
},
}),
],
};
引入第三方模块
- 方式一: 直接引入
import _ from 'lodash'把第三方库直接打包到了输出文件呢,特别的大 - 方式二: 插件引入 --- ProvidePlugin。会自动向模块内部注入 lodash 模块,在模块内部可以通过*引用
new webpack.ProvidePlugin({'*':'lodash'})优点: 不需要在每个模块内部导入,可以直接使用 缺点: 也会把 lodash 打包到输出文件当中 没有全局的*变量,比如在 html 文件中就无法通过 window.*来引入 没有全局的函数变量,所以导入依赖全局变量的插件依旧会失效
- 方式三: expose-loader
expose-loader 可以把模块添加到全局对象上,在调试的时候比较有用 不需要任何其他插件配合,只要将下面的代码添加到所有的 loader 之前 还是会打包到输出文件中 还是需要在模块内至少手动引用一次,会把变量挂在全局变量 window 上
// 需要在文件模块中 require("lodash")
export default {
entry,
output,
plugins,
mudule: {
rules: [
{
test: require.resolve('lodash'), // 返回的是模块的入口文件的绝对路径
loader: 'expose-loader', // 暴露的loader,可以向window上挂载变量
options: {
exposes: {
globalName: '_',
override: true,
},
},
},
],
},
};
- 方式四: 手动在 html 中引入 cdn 脚本,并且配合 externals,不需要引入
// html
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
<script> console.log(window.jQuery) </script>
// webpack.config.js
export default {
entry,
output,
module,
plugins,
externals: {
lodash: "_", // 如果在模块内部引用了lodash这个模块,会从window._上取值
jquery: "jQuery", // 如果在模块内部引用了jquery这个模块,会从window.jQuery上取值
}
}
- 方式五: 借助 html-webpack-externals-plugin
// webpack.config.plugin
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
export default {
entry,
output,
module,
plugins: [
// 这个插件不能用了,因为html-webpack-plugin版本冲突(webpack5 对应html-webpack-plugin 5版本,但是html-webpack-externals-plugin依赖html-webpack-plugin2版本)
// 等讲插件的时候可以基于最新的版本写一个HtmlWebpackExternalsPlugin
new HtmlWebpackExternalsPlugin({
externals: [
//定义外部全局变量
{
module: 'lodash', // 模块名
entry: 'https://cdn.bootcss.com/jquery/3.4.1/jquery.js', // cdn地址
global: '_', // 全局变量名
},
{
module: 'jquery',
entry: 'https://cdn.bootcss.com/jquery/3.4.1/jquery.js', // cdn地址
global: '$',
},
],
}),
],
};
watch 参数
- webpack 定时获取文件的更新时间,并跟上次保存的事件进行对比,不一致就表示发生了变化。poll 就是用来配置每秒问多少次
- 当检测文件不再发生变化,会先缓存起来,等待一段时间之后再通知监听者,这个等待时间通过 aggregateTimeout 配置
- webpack 只会监听 entry 依赖的文件
- 我们需要尽可能减少需要监听的文件数量和检查频率,当然频率的降低会导致灵敏度下降
// webapack.config.js
export default {
entry,
output,
module,
plugins,
watch: true, // 开启监控模式
watchOptions: {
ignored: /node_modules/, //忽略变化的文件夹
aggregateTimeout: 300, // 监听到变化后会等300毫秒再去执行(其实是一个防抖的优化)
poll: 1000, // 每秒问操作系统多少次文件是否已经变更
},
};
添加商标 webpack.BannerPlugin
- new webpack.BannerPlugin('珠峰架构')
拷贝静态文件
- copy-webpack-plugin可以拷贝源文件到目标目录
// webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');
export default {
entry,
output,
module,
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, 'src/static'), // 静态资源目录源地址
to: path.resolve(__dirname, 'dist/static'), // 目标地址,相对于output的path目录
},
],
}),
],
};
打包前先清空输出目录 -- clean-webpack-plugin
// webpack.config.js
const { CleanWebapckPlugin } = require('clean-webpack-plugin');
export default {
entry,
output,
module,
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['**/*'],
}),
],
};
服务器代理
- 如果你有单独的后端开发服务器 API,并且希望在同域名下发送 API 请求,那么代理某些 URL 会很有用
- 不修改路径
请求到 /api/users 现在会被代理到请求 http://localhost:3000/api/user
devServer: { proxy: { "/api": "http://localhost:3000" }} - 修改路径
export default {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: { '^/api': '' },
},
},
},
};
- before / after
before 在 webpack-dev-server 静态资源中间件处理之前,可以用于拦截部分请求返回特定内容,或者实现简单的数据 mock before 是在静态资源中间件之前,一般用来配置 mock 数据或者配置一些中间件 after 是在静态资源中间件之后,一般用来进行一些异常处理,记录一些日志 基本没人用
export default {
devServer: {
before(app) {
// webpack-dev-server本质上是一个express服务器
app.get('/api/users1', function (req, res) {
res.json([{ id: 1, name: 'zhuzhubefore' }]);
});
},
// 中间就是静态文件 即产出的文件
after(app) {
app.get('/api/users2', function (req, res) {
res.json([{ id: 1, name: 'zhuzhuafter' }]);
});
},
},
};
webpack-dev-middleware
- 就是在 express 中提供 webpack-dev-server 静态服务能力的一个中间件
- webpack-dev-server 的好处是相对简单,直接安装依赖后执行命令即可
- 使用 webpack-dev-middleware 的好处是可以在既有的 express 代码基础上快速添加 webpack-dev-middleware 的功能,同时利用 express 来根据需要添加更多的功能,如 mock 服务/代理 api 请求等
const express = require('express');
const app = express();
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackOptions = require('./webpack.config');
webpackOptions.mode = 'development';
const compiler = webpack(webpackOptions);
app.use(webpackDevMiddleware(compiler, {}));
app.listen(3000);
生产环境配置
提取 css
- 因为 css 的下载和 js 可以并行,当一个 html 文件很大的时候,可以把 css 单独提取出来加载
- 安装: mini-css-extract-plugin 只负责提取 css
// webpack.config.js
const MiniCssExtractPlugin = require('minii-css-extract-plugin');
export default {
entry,
output,
module: {
rules: [
{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] },
{
test: /\.less$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
},
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css', // main.css
}),
],
};
指定图片和 css 目录
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
export default {
entry,
output,
module: {
rules: [
{
test: /\.(jpg|png|bmp|gif|svg)$/,
use: [
{
loader: 'url-loader',
options: {
esModule: false,
name: '[hash:10].[ext]',
limit: 8 * 1024,
outputPath: 'images', //默认是在打包目录下,配置之后指定写入到输出目录images里
publicPath: '/images', // 配置了outputPath,就要加上publicPath,否则文件引用路径会出问题
},
},
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
// filename: '[name].css', // main.css
filename: 'css/[name].css', //指定css目录
}),
],
};
hash / chunkhash / contenthash
- 三种 hash 从左往右稳定性越来越强,缓存性越来越强,性能越来越差
- 文件指纹是指打包后输出的文件名和后缀
- hash 一般是结合 cdn 缓存来使用。通过 webpack 构建之后,生成对应文件名自动带上对应的 md5 值。如果文件内容改变的话,那么对应文件哈希值也会改变,对应的 html 引用的 url 地址也会改变,触发 cdn 服务器从源服务器上拉取对应数据,进而更新本地缓存
| 占位符名称 | 含义 |
|---|---|
| ext | 资源后缀名 |
| name | 文件名称 |
| path | 文件的相对路径 |
| folder | 文件所在文件夹 |
| hash | 每次 webpack 构建时生成一个唯一的 hash 值 |
| chunkhash | 根据 chunk 生成 hash 值,来源于同一个 chunk,则 hash 值就一样 |
| contenthash | 根据内容生成 hash 值,文件内容相同 hash 值就相同 |
- hash
如果使用 hash,那么它是工程级别的,修改任何一个文件,所有的文件名都会发生改变
- chunkhash
代码块 hash 一个入口和它所以来的模块组成一个代码块 chunk。会根据不同的入口文件,进行依赖文件解析,构建对应的 hash 值 根据不同入口文件进行依赖文件解析,构建对应的哈希值。我们在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着采用 chunkhash 生成哈希值,那么只要我们不改动公共库代码就可以保证其哈希值不会受影响
- contenthash
内容 hash 对应打包后的文件 使用 chunkhash 存在一个问题,就是当一个 js 文件中引入 css 文件,编译后他们的 hash 是相同的,而且只要 js 文件发生变化,关联的 css 文件 hash 也会改变。这个时候可以使用 mini-css-extract-plugin 配合 contenthash,保证即使 css 文件所处的模块里就算其他文件内容改变,只要 css 文件内容不变,那么就不会重复构建
css 兼容性
- 为了浏览器的兼容性,有时必须加入-webkit,-ms,-o,-moz 这些前缀
- 伪元素::placeholder 可以选择一个表单元素的占位文本,允许开发者和设计师自定义占位文本的样式 有兼容性问题
- 安装
postcss-loader 可以使用 postcss 处理 css postcss-preset-env 把现代的 css 转换成大多数浏览器能理解的 postcss preset env 已经包含了 autoprefixer 和 browsers 选项 npm i postcss-loader postcss-preset-env -D
// package.json
"browserslist": {
"development": [
"last 1 chrome version", // 最新的chrome版本
"last 1 firefox version",
"last 1 safari version",
],
"production": [
">0.2%"
]
}
// 也可配置postcss.config.js
let postcssPresetEnv = require("postcss-preset-env");
module.exports = {
plugins: [
postcssPresetEnv({
browsers: 'last 5 version'
})
]
}
// webpack.config.js
module.exports = {
mode,
entry,
output,
plugins,
module: {
rules: [
{test: /\.css$/, use: ['style-loader', 'css-loader', 'postcss-loader']}
]
}
}
压缩 js/css/html
- optimize-css-assets-webpack-plugin是一个优化和压缩 css 资源的插件
- terser-webpack-plugin是一个优化和压缩 js 资源的插件, 已内置
- 如果 mode=production,css/js/html 默认会自动压缩,不需要配置,mode=none 或者 development 就需要自己配置了
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'none',
optimization: {
// 压缩js
minimize: true, //启用最小化
minimizer: [
new TerserPlugin(), // 以前是uglifyjs,不支持es6 用于压缩js
],
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
minify: {
// 压缩html
collapseWhitespace: true, //删除空格
removeComments: true, // 删除注释
},
}),
new OptimizeCssAssetsWebpackPlugin(), // 压缩css
],
};
压缩图片 -- 一般不用
- image-webpack-loader可以帮助对图片进行压缩和优化
- npm i image-webpack-loader --dave-dev
- 一般不会用到,图片一般不用 webpack 进行压缩
{
test: /\.(png|svg|jpg|gif|jpeg|ico)$/,
use: [
"url-loader",
{
loader: "image-webpack-loader",
options: {
mozjpeg: {
progressive: true,
quality: 65
},
optipng: {
enabled: false
},
pngquant: {
quality: "65-90",
speed: 4
},
gifsicle: {
interlaced: false
},
webp: {
quality: 75
}
}
}
]
}
px 自动转成 rem
- lib-flexible + rem 实现移动端自适应
- px2rem-loader 自动将 px 转换成 rem
- 页面渲染式计算根元素的 font-size 值
- npm i px2rem-loader lib-flexible -D
// webpack.config.jd
{
test: /\.css$/,
use: [
"style-loader",
"css-loader",
"postcss-loader",
{
loader: "px2rem-loader",
options: {
remUnit: 75, // 一个rem是多少像素
remPrecision: 8, //计算rem的单位,保留几位小数 设置精度
}
}
]
}
<!-- index.html -->
<head>
<script>
let docElement = document.documentElement; //根元素
function setRemUnit() {
// 把根元素的字体大小设置为宽度的十分之一
docElement.style.fontSize = docElement.clientWidth / 10 + 'px';
}
setRemUnit();
window.addEventListener('resize', setRemUnit);
</script>
</head>
polyfill
@babel/polyfill
babel默认只转换新的javascript语法(如箭头函数),而不转换新的 api,比如 Iterator/Generator/Set/Map/Proxy/Reflect/Symbol/Promise 等全局对象,以及一些在全局对象上的方法(比如 Object.assign)都不会转码- 比如:es6 在 Array 对象上新增了 Array.from 方法,Babel 就不会转码这个方法,如果想让这个方法运行,必须使用
babel-polyfill来转换等 babel-polyfill是通过向全局对象和内置对象的prototype上添加方法来实现的,比如运行环境中不支持 Array.prototype.find 方法,引入 polyfill,我们就可以使用 es6 方法来编写了,但是缺点就是会造成全局空间污染- @babel/preset-env 为每一个环境的预设
- @babel/preset-env 默认支持语法转化,需要开启
useBuiltIns配置才能转化 API 和实例方法 - useBuiltIns 可选值包括: usage/entry/false,默认为 false,表示不对 polyfill 处理,这个配置是引入 polyfill 的关键
- 安装: npm i @babel/polyfill
useBuiltIns
- 涉及三个概念
最新的 es 语法:如箭头函数 最新的 es api: 如 Promise 最新的 es 实例方法:如 String.prototype.includes
- useBuiltIns 如果不设置,默认为 false
@babel/preset-env 只转换新的语法,不转换 API 和方法 如果手动设置 useBuiltIns:false 还想实现 api 和方法的兼容性处理,要自己引入:
import '@babel/polyfill'useBuiltIns:false; 手动 import '@babel/polyfill'; 会无视兼容性配置(指的是 package.json 中的 browserslist),直接引入所有的 polyfill,不管需不需要,一股脑全部引入 - useBuiltIns:"entry"
在项目入口引入一次(多次引入会报错) useBuiltIns:"entry" 会根据配置的浏览器兼容,引入浏览器不兼容的 polyfill。需要在入口文件手动添加 import '@babel/polyfill',会自动根据 browserslist 替换成浏览器不兼容的所有 polyfill corejs 是腻子的实现,2 是实现的版本。老的版本是 2,新的版本是 3 corejs 默认是 2,配置 2 的话需要单独安装 corejs@3 (npm i core- js@3) corejs 是自动安装的,安装@babel/preset-env 之后就会安装 corejs2 版本corejs3 不会自动安装,需要手动安装 这里需要指定 core-js 版本,如果"corejs":3,则 import '@babel/polyfill'需要改成
import 'core-js/stable'; import 'regenerator-runtime/runtime'不管项目中有没有用到,不兼容的都会被引入,因为是严格按照浏览器的要求引入的 polyfill,浏览器缺什么就会引入什么,跟项目中有没有用到没有关系
// 入口文件 index.js
import '@babel/polyfill'; // corejs2
import 'core-js/stable'; // corejs3
import 'regenerator-runtime/runtime'; // corejs3
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'entry',
corejs: 3,
},
],
'@babel/preset-react',
],
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }],
['@babel/plugin-proposal-class-properties', { loose: true }],
],
},
},
},
],
},
};
-
useBuiltIns:"usage"
usage 会根据匹配的浏览器兼容,以及你代码中用到的 api 进行 polyfill 实现按需加载 当设置 usage 时,polyfills 会自动按需添加,不再需要手动引入
@babel/polyfillusage 的行为类似于babel-transform-runtime不会造成全局污染 因此也不会对蕾丝 Array.prototype.includes 进行 polyfill -
usage 和 entry 有一个本质区别
entry 是全局引入,只需要在入口文件里单独引入一次就可以,所有的地方都可以使用 usage 是局部引入, 100 个模块都是用到 Promise,就会被引入 100 次, 所以 usage 可 能会增加文件体积(只是多增加了引入代码而已,代码量不会成倍增加)
-
以上的配置是自己控制不了的,是 preset-env 自己引入的,如果想实现选择加载自己想要加载的一些方法进行 polyfill,可以使用
babel-runtime
babel-runtime
- babel 为了解决全局空间污染的问题,提供了单独的包
babel-runtime用以提供编译模块的工具函数 - 简单的说 babel-runtime 更像是一种按需加载的实现,比如哪里需要使用 Promise 只要在这个文件头部
import Promise from 'babel-runtime/core-js/promise' - 即哪里需要自己引入
babel-plugin-transform-runtime
- 用这个插件就不再需要配置 useBuiltIns 了
- @babel/plugin-transform-runtime 插件是为了解决
多个文件重复引入相同 helpers(帮助函数) => 提取运行时 新 api 方法全局污染 => 局部引入
- 启用插件 babel-plugin-transform-runtime 后,babel 就会使用 babel-runtime 下的工具函数
- babel-plugin-transform-runtime 插件能够将这些工具函数的代码转换成 require 语句,指向为对 babel-runtime 的引用
- babel-plugin-transform-runtime 就是可以在我们使用 api 时自动 import babel-runtime 里的 polyfill
当我们使用 async/await 时,自动引入 babel-runtime/regenerator 当我们使用 es6 的静态事件或者内置对象时,自动引入 babel-runtime/core-js 移除内联 babel helpers 并使用 babel-runtime/helpers 来替换
- corejs 默认是 3,配置 2 的话需要单独安装 @babel/runtime-corejs2
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: [
[
'@babel/plugin-transform-runtime',
{
corejs: 3, // 当我们使用es6的静态事件或者内置对象时,自动引入babel-runtime/core-js
helpers: true, // 移除内联babel helpers并使用babel-runtime/helpers来替换
regenerator: true, // 是否开启generator函数转换成使用regenerator runtime来避免污染全局
},
],
['@babel/plugin-proposal-decorators', { legacy: true }],
['@babel/plugin-proposal-class-properties', { loose: true }],
],
},
},
},
],
},
};
如何选择最适合的配置
- babel-runtime 适合在组件和类库中使用,局部引入,不污染全局
- babel-polyfill 适合在业务项目中使用,不怕污染全局,所以可以使用
- 局部引入 优点是不污染全局,缺点是增加文件体积
- 全局引入 优点是降低文件体积,缺点是污染全局
polyfill-service
- polyfill.io 自动化的 javascript polyfill 服务
- polyfill.io 通过分析请求头信息中的 UserAgent 实现自动加载浏览器所需的 polyfills
<script src="https://polyfill.io/v3/polyfill.min.js"></script>
小结
- babel-polyfill
- preset-env
- useBuiltIns false import babel/polyfill entry 只入口引入一次 corejs2 import babel/polyfill corejs3 import corejs generator usage 按需引入 不需要自己引入,会根据使用了哪些功能自动引入
- babel-runtime 需要手动引入
- babel-plugin-transform-runtime 自动分析使用了哪些 局部引入
- preset-env usebuiltins usage 与 preset-env + babel-plugin-transform-runtime 效果基本上是一样的 按需引入 局部引入
webpack 原理篇
webpack 介绍
- webpack 是一个前端资源加载和打包工具,他根据模块的依赖关系进行静态分析,然后将这些模块按照制定的规则生成对应的静态资源
预备知识
toStringTag
- Symbol.toStringTag 是一个内置的 symbol,它通常作为对象的属性键使用,对应的属性值应该是字符串类型,这个字符串用来表示该对象的自定义类型标签
- 通常只有内置的 Object.prototype.toString()方法会去读取这个标签并把它包含在自己的返回值中
Object.prototype.toString.call('foo'); // "[object String]"
Object.prototype.toString.call([1, 2, 3]); // "[object Array]"
Object.prototype.toString.call(1); // "[object Number]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
let myExports = {};
Object.defineProperty(myExports, Symbol.toStringTag, { value: 'Module' });
console.log(Object.prototype.toString.call(myExports)); // "[object Module]"
defineProperty
- defineProperty 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象
Object.create(null)
- 使用 create 创建的对象,没有任何属性,把它当作一个非常纯净的 map 来使用,可以定义 hasOwnProperty、toString 方法,完全不必担心会将原型链上的同名方法覆盖掉
- 在我们使用 for..in 循环的时候会遍历对象原型链上的属性,使用 create(null)就不必再对属性进行检查了
var ns = Object.create(null);
if (typeof Object.create !== 'function') {
Object.create = function (proto) {
function F() {}
F.prototype = proto;
return new F();
};
}
console.log(ns);
console.log(Object.getPrototypeOf(ns));
同步加载
- 打包后的文件 首先是一个自执行函数
// index.js
let title = require('./title');
console.log(title);
// title.js
module.exports = 'title';
// 对于我们自己的模块 模块id是相对于根目录的相对路径
var modules = {
'./src/title.js': function (module, exports, require) {
// 函数里面是title.js文件里的内容
module.exports = 'title';
},
};
// 缓存对象
// 模块加载后会把加载到的结果放在缓存对象cache里
let cache = {};
// 因为commonjs浏览器是不识别的,不能识别require方法,所以需要自己实现一个浏览器能够识别的require方法
function require(moduleId) {
// 如果缓存中有就直接使用
if (cahce[moduleId] !== undefined) {
return cache[moduleId].exports;
}
let module = (cache[moduleId] = { exports: {} });
modules[moduleId](module, module.exports, require);
return module.exports;
}
// 执行入口文件的代码(即index.js中的代码)
let title = require('./src/title.js');
console.log(title);
同步加载的模块兼容性实现
- 压缩只能压缩变量,不能压缩属性,所以只能在定义属性的时候短一点,金科鞥减少体积(require.r require.o require.d)
- js 源代码首先会走 babel 转换,babel 可能会把 esm 转换成 commonjs,可能不转换;webpack 都会转成 commonjs
- 在 webpack 中既支持 commonjs 也支持 esmodules,说明他们是可以互相转换的,所以可以
commonjs 加载 commonjs common.js 加载 ES6 modules
将 esmodule 编译成 commonjs: 涉及 require.r require.d 方法 给 exports 对象上挂属性 只要模块内出现了 import 或者 export 那就是 esmodule ES6 modules 加载 ES6 modules 实现和 commonjs 加载 esm 基本一致,只是加了将 index.js 入口文件表示为 esmodule
var exports = {}; require.r(exports);ES6 modules 加载 common.js commonjs 打包之前之后基本没有任何区别,不需要转换
commonjs 加载 commonjs
// index.js
let title = require('./title');
console.log(title.name);
console.log(title.age);
// title.js
exports.name = 'title_name';
exports.age = 'title_age';
// 实现和上面的同步加载一致
common.js 加载 ES6 modules
// index.js
// 通过commonjs导入title模块
let title = require('./title');
console.log(title.default);
console.log(title.age);
// title.js
export default 'title_defualt';
export const age = 'title_age';
// 编译实现
// 将esmodule编译成commonjs: 涉及 require.r require.d方法,给exports对象上挂属性
var modules = {
'./src/title.js': function (module, exports, require) {
require.r(exports); // 标示 这是一个es模块 webpack中所有属性方法 都是一个字母
require.d(exports, {
default: () => DEFAULT_EXPORTS,
age: () => age,
});
const DEFAULT_EXPORTS = 'title_defalt';
const age = 'title_age';
},
};
var cache = {};
function require(moduleId) {
var cacheModule = cache[moduleId];
if (cacheModule !== undefined) {
return cacheModule.exports;
}
var module = (cache[moduleId] = { exports: {} });
modules[moduleId](module, module.exports, require);
return module.exports;
}
require.r = (exports) => {
// 表示es6 modules
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); // toString时输出 [object Module]
Object.defineProperty(exports, '__esModule', { value: true }); // exports.__esModule = true
};
require.d = (exports, definition) => {
for (let key in definition) {
Object.defineProperty(exports, key, { get: definition[key] });
}
};
let title = require('./src/title.js');
console.log(title.default);
console.log(title.age);
ES6 modules 加载 ES6 modules
// index.js
import title, { age } from './title';
console.log(title);
console.log(age);
// title.js
export default 'title_defualt';
export const age = 'title_age';
// 编译实现
var modules = {
'./src/title.js': function (module, exports, require) {
require.r(exports); // 标示 这是一个es模块 webpack中所有属性方法 都是一个字母
require.d(exports, {
default: () => DEFAULT_EXPORTS,
age: () => age,
});
const DEFAULT_EXPORTS = 'title_defalt';
const age = 'title_age';
},
};
var cache = {};
function require(moduleId) {
var cacheModule = cache[moduleId];
if (cacheModule !== undefined) {
return cacheModule.exports;
}
var module = (cache[moduleId] = { exports: {} });
modules[moduleId](module, module.exports, require);
return module.exports;
}
require.r = (exports) => {
// 表示es6 modules
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); // toString时输出 [object Module]
Object.defineProperty(exports, '__esModule', { value: true }); // exports.__esModule = true
};
require.o = (obj, prop) => obj.hasOwnProperty(prop); // 可有可无,不重要
require.d = (exports, definition) => {
for (let key in definition) {
if (require.o(definition, key)) {
Object.defineProperty(exports, key, { get: definition[key] });
}
}
};
var exports = {}; // +
require.r(exports); // + 将index.js标示为esmodule
let title = require('./src/title.js');
console.log(title.default);
console.log(title.age);
ES6 modules 加载 common.js
// index.js
import title, { age } from './title';
console.log(title); // {name: "title_name", age:"title_age"}
console.log(age); // 'title_age'
// title.js
// exports.name = 'title_name';
// exports.age = 'title_age'
module.exports = {
name: 'title_name',
age: 'title_age',
};
// 编译实现
var modules = {
'./src/title.js': function (module, exports, require) {
module.exports = {
name: 'title_name',
age: 'title_age',
};
},
};
var cache = {};
function require(moduleId) {
var cacheModule = cache[moduleId];
if (cacheModule !== undefined) {
return cacheModule.exports;
}
var module = (cache[moduleId] = { exports: {} });
modules[moduleId](module, module.exports, require);
return module.exports;
}
require.r = (exports) => {
// 表示es6 modules
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); // toString时输出 [object Module]
Object.defineProperty(exports, '__esModule', { value: true }); // exports.__esModule = true
};
require.o = (obj, prop) => obj.hasOwnProperty(prop); // 可有可无,不重要
require.d = (exports, definition) => {
for (let key in definition) {
if (require.o(definition, key)) {
Object.defineProperty(exports, key, { get: definition[key] });
}
}
};
// 返回获取default默认导出的getter方法
// esm取module.default,commonjs取module本身
require.n = function (module) {
let getter = module.__esModule ? () => module.default : () => module;
return getter;
};
var exports = {};
require.r(exports);
let title = require('./src/title.js');
let title_default = require.n(title); // +
console.log(title_default()); // +
console.log(title.age);
异步加载
- 通过 import 加载的代码块的 chunkId 是如何计算的?
- 得到加载模块相对于根目录的相对路径 ./src/title.js
- 将./和/转成下划线 src_title_js, 这就是 chunkId
// index.js
document.addEventListener('click', () => {
import('./title').then((result) => {
console.log(result.default);
});
});
// title.js
export default 'title';
// 简易实现
// 在原始的mian.js里没有任何模块定义(因为例子中只有一个title.js,还是异步加载,所以这里开始是空)
var modules = {};
// 缓存对象
var cache = {};
// 能在浏览器中跑的require方法
function require(moduleId) {
var cacheModule = cache[moduleId];
if (cacheModule !== undefined) {
return cacheModule.exports;
}
var module = (cache[moduleId] = { exports: {} });
modules[moduleId](module, module.exports, require);
return module.exports;
}
require.r = (exports) => {
// 表示es6 modules
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); // toString时输出 [object Module]
Object.defineProperty(exports, '__esModule', { value: true }); // exports.__esModule = true
};
// 返回获取default默认导出的getter方法
require.n = function (module) {
let getter = module.__esModule ? () => module.default : () => module;
return getter;
};
// 通过require.m属性可以获取模块定义(在HMR有用)
require.m = moudles;
// 判断对象上有没有某个属性
require.o = (obj, prop) => obj.hasOwnProperty(prop);
// 给对象上定义属性
require.d = (exports, definition) => {
for (let key in definition) {
if (require.o(definition, key)) {
Object.defineProperty(exports, key, { get: definition[key] });
}
}
};
require.f = {};
// 在一个项目中可能会有很多的代码块,每个代码块都会有状态
// key是代码块的名字, 0表示此代码块已经加载完成
let installedChunks = {
main: 0, // 当前的入口代码块,他肯定是加载完成的
};
// 通过jsonp加载chunk代码,并且创建promise放到数组里
require.f.j = (chunkId, promises) => {
// 先判断installedChunks中有没有加载了或者正在加载的data,如果有直接复用
let installedChunkData = require.o(installedChunks, chunkId)
? installedChunks[chunkId]
: undefined;
if (installedChunkData !== 0) {
// !=0表示没有加载完成
// 第二次以及以后懒加载
if (installedChunkData) {
// !=0但是有值,说明很可能正在加载中
promises.push(installedChunkData[2]); // 直接取出上一个promise放到数组中
} else {
// 第一次懒加载时 installedChunkData=undefined
let promise = new Promise((resolve, reject) => {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise; // installedChunkData = [resolve, reject, promise]
promises.push(promise);
// 开始加载
let url = require.p + require.u(chunkId);
require.l(url);
}
}
};
// 通过jsonp异步加载代码块 chunkId=src_title_js
require.e = (chunkId) => {
let promises = [];
Object.keys(require.f).forEach((func) => func(chunkId, promises));
return Promise.all(promises);
};
// 文件名
require.u = (chunkId) => '' + chunkId + '.js';
// 路径前缀
// 就是webpack.config.js中 output里面配置的publicPath的值,没有配置就是空
require.p = '';
// 加载
require.l = (url, done, key, chunkId) => {
// jsonp原理
let script = document.createElement('script');
script.src = url;
document.head.appendChild(script);
};
function webpackJsonpCallback(data) {
let [chunkIds, moreModules] = data;
for (let moduleId in moreModules) {
// 模块合并
require.m[moduleId] = moreModules[moduleId];
}
for (let chunkId, i = 0; i < chunkIds.length; i++) {
chunkId = chunkIds[i]; // src_title_js
installedChunks[chunkId][0](); // 让这个resolve对应的promise变成成功态
installedChunks[chunkId] = 0; // 加载完成
}
}
var chunkLoadingGlobal = (window['webpack5'] = []);
chunkLoadingGlobal.push = webpackJsonpCallback;
var exports = {};
document.addEventListener('click', () => {
require
.e('src_title_js')
.then(() => require('./src/title.js'))
.then((result) => {
console.log(result.default);
});
});
// src_title_js.js文件
// "webpack5"名字随意
window['webpack5'].push([
['src_title_js'],
{
'./src/title.js': (module, exports, require) => {
require.r(exports);
require.d(exports, {
default: () => DEFAULT_EXPORTS,
});
const DEFAULT_EXPORTS = 'title';
},
},
]);
// 面试被问 webpack 编译后的代码风格是原来的哪种 js 的写法
抽象语法树
- webpack 和 lint 等很多工具和库的核心都是通过 AST 抽象语法树这个概念来实现对代码的检查和分析等操作的
- 通过了解抽象语法树这个概念,你也可以随手编写类似的工具
抽象语法树用途
- 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
- 如 JSLint、JSHint 对代码错误或风格的检查,发现一些潜在的错误
- IDE 的错误提示、格式化、高亮、自动补全等等
- 代码混淆压缩 -UglifyJS2 等
- 优化变更代码,改变代码结构使达到想要的结构
- 代码打包工具 webpack、rollup 等等
- CommonJS、AMD、CMD、UMD 等代码规范之间的转化
- CoffeeScript、TypeScript、JSX 等转化为原生 Javascript
抽象语法树的定义
- 这些工具的原理都是通过 javascript Parser 把代码转化成一棵抽象语法树(AST)。这棵树定义了代码的结构,通过操纵这棵树,可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作。
javascript Parser
- javascript Parser,把 js 源码转化为抽象语法树的解析器
- 浏览器会把 js 源码通过解析器转为抽象语法树,再进一步转化为字节码或者直接生成机器码
- 一般来说每个 js 引擎都会有自己的抽象语法树格式,chrome 的 v8 引擎,firefox 的 SpiderMonkey 引擎等等。
- 常见的 javascript Parser
- esprima
- traceur
- acorn
- shift
- esprima
通过 esprima 把源码转化成 AST 通过 estraverse 遍历并更新 AST 通过 escodegen 将 AST 重新生成源码 astexplorer AST 的可视化工具 astexplorer.net/
// mkdir zhufengast
// cd zhufengast
// cnpm i esprima estraverse escodegen- S
let esprima = require('esprima');
var estraverse = require('estraverse');
var escodegen = require('escodegen');
let code = 'function ast(){}';
let ast = esprima.parse(code);
let indent = 0;
function pad() {
return ' '.repeat(indent);
}
// estraverse会以深度优先的方式遍历语法树所有节点
// 每个节点会有进入和离开两个步骤
estraverse.traverse(ast, {
enter(node) {
console.log(pad() + node.type, '进入');
if (node.type == 'FunctionDeclaration') {
node.id.name = 'ast_rname';
}
indent += 2;
},
leave(node) {
indent -= 2;
console.log(pad() + node.type, '离开');
},
});
// Program
// FunctionDeclaration
// Identifier
// Identifier
// BlockStatement
// BlockStatement
// FunctionDeclaration
// Program
let generated = escodegen.generate(ast);
console.log(generated);
Babel
- babel 能够转译 ecmascript2015+ 的代码,使他在旧的浏览器或者环境中也能运行
- 工作过程分为三部分
Parse(解析) 将源代码转换成抽象语法树,树上有很多的 estree 节点 Transform(转换) 对抽象语法树进行转换 Generate(代码生成) 将上一个经过转换的抽象语法树生成新的代码
babel 插件
@babel/corebabel 的编译器,核心 api 都在这里面,比如常见的 transform、parse核心库,提供语法树的生成/遍历功能
babylonbabel 的解析器 -- 类似于 esprimababel-types用于 AST 节点的 lodash 式工具库,它包含了构造、验证以及变换 AST 节点的方法,对编写处理 AST 逻辑非常有用工具库,帮助生成相应节点
babel-traverse用于对 AST 的遍历,维护整棵树的状态,并且负责替换、移除、添加节点 -- 类似于 estraverse
转换箭头函数
- @babel/core 提供 transform 方法,里面包含了所有的流程
根据源代码生成老的语法树 遍历老的语法树 遍历的时候要找你注册的插件 找这些插件指定
- 插件的核心就是将老的语法树转成新的语法树,比较新老语法树的差异,以最小的代价转换过来,尽可能复用
- 所谓的 babel 插件就是一个对象, 里面有属性 visitor 对象
- 当 babel 在遍历语法树的时候,会看有没有插件里的访问器,拦截节点,如果有的话就会把对应的节点路径传给此函数
- babel-plugin-transform-es2015-arrow-functions
- npm i @babel/core babel-types -D
// 转换前
const sum = (a, b) => a + b;
// 转换后
const sum = function sum(a, b) {
return a + b;
};
// 实现
let babel = reuqire('@babel/core');
const code = `const sum = (a,b) => a+b`;
let ArrowFunctionPlugin = {
visitor: {
ArrowFunctionExpression(nodePath) {
let node = nodePath.node;
node.type = 'FunctionExpression';
},
},
};
const result = babel.transform(code, {
plugins: [ArrowFunctionPlugin],
});
console.log(result.code);
// 转换前
const sum = (a, b) => {
console.log(this);
return a + b;
};
// 转换后
var _this = this;
const sum = function (a, b) {
console.log(_this);
return a + b;
};
let babel = reuqire('@babel/core');
let types = require('babel-types');
let ArrowFunctionPlugin2 = {
visitor: {
ArrowFunctionExpression(nodePath) {
let node = nodePath.node; // 获取节点
const thisBinding = hoistFunctionEnvironment(nodePath);
node.type = 'FunctionExpression';
},
},
};
function hoistFunctionEnvironment(fnPath) {
// Program
// 从当前节点向上查找
const thisEnvFn = fnPath.findParent((p) => {
// 是一个函数 不能是箭头函数 或者 是根节点也可以
return (p.isFunction() && !p.isArrowFunctionExpression()) || p.isProgram();
});
// 从当前节点向下查找
// 找当前作用域哪些地方用到了this
let thisPaths = getScopeInfoInfomation(fnPath);
// 声明了一个this的别名变量 默认是_this 真实源代码中会判断 如果_this被占用了就用_this2
let thisBinding = '_this';
if (thisPaths.length > 0) {
// 向thisEnvFn这个作用域内添加一个变量
// 变量名为_this,初始化的值为this
thisEnvFn.scope.push({
// 箭头函数里面嵌套箭头函数不会添加重复代码
id: types.identifier(thisBinding),
init: types.thisExpression(),
});
thisPaths.forEach((thisPath) => {
// 创建一个_this的标识符
let thisBindingRef = types.identifier(thisBinding);
// 把老得路径上的节点替换成新节点
thisPath.replaceWith(thisBindingRef);
});
}
}
function getScopeInfoInfomation(fnPath) {
let thisPaths = [];
// 遍历当前path所有的子节点路径
fnPath.traverse({
ThisExpression(thisPath) {
thisPaths.push(thisPath);
},
});
return thisPaths;
}
const result = babel.transform(code, {
plugins: [ArrowFunctionPlugin2],
});
console.log(result.code);
把类编译成 Function
- @babel/plugin-transform-classes
// 编译前
class Person {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
// 编译后
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
};
let babel = require('@babel/core');
let t = require('babel-types');
let source = `
class Person {
constructor(name) {
this.name=name;
}
getName() {
return this.name;
}
}
`;
let transformClasses = (state, opts) => {
return {
visitor: {
ClassDeclaration(nodePath) {
let { node } = nodePath;
let id = node.id; // {type: 'Identifier', name: 'Person'}
let methods = node.body.body; // 拿到类上的方法
let nodes = [];
methods.forEach((classMethod) => {
if (classMethod.kind === 'constructor') {
// 是构造函数
let constructorFunction = types.functionDeclaration(
id,
classMethod.params,
classMethod.body,
classMethod.generator,
classMethod.async
);
nodes.push(constructorFunction);
} else {
let right = types.functionExpression(
id,
classMethod.params,
classMethod.body,
classMethod.generator,
classMethod.async
);
let prototype = types.memberExpression(
id,
types.identifier('prototype')
);
let left = types.memberExpression(prototype, classMethod.key);
let assignmentExpression = types.assignmentExpression(
'=',
left,
right
);
nodes.push(assignmentExpression);
}
}); // 为什么不用构建ExpressionStatement?而是直接构建AssignmentExpression????????????
if (nodes.length === 1) {
nodePath.replaceWith(nodes);
} else {
nodePath.replaceWithMultiple(nodes);
}
},
},
};
};
const result = babel.transform(source, {
plugins: [transformClasses],
});
console.log(result.code);
webpack TreeShaking 插件(是 babel 插件)
- 实现按需加载
import { flatten,concat } from "lodash"转换成:import flatten from "lodash/flatten";>import concat from "lodash/flatten"; - webpack 配置按需加载: babel-plugin-import
// webpack.config.js
// 编译顺序为首先plugins从左往右,然后presets从右往左
module.exports = {
mode,
entry,
output,
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
plugins: [['import', { library: 'lodash' }]],
// 配置自定义babel插件
// plugins: [
// [
// path.resolve(__dirname, 'plugins/babel-plugin-import.js'),
// {
// library: 'lodash'
// }
// ]
// ]
},
},
},
],
},
};
- 实现按需加载的 babel 插件
let babel = require('@babel/core');
let t = require('babel-types');
let visitor = {
ImportDeclaration(nodePath, state) {
let { opts } = state; //传入的参数
let { node } = nodePath;
let specifiers = node.specifiers; // flatten、concat
let source = node.source; // lodash
// 只有第一个specifiers不是默认导入的才会进来
// 如果已经转换过了 已经把一个普通导入变成默认导入 那就不要进来了
if (
opts.library === source.value &&
!t.isImportDefaultSpecifier(specifiers[0])
) {
const importDeclaration = specifiers.map((specifier, index) => {
return t.ImportDeclaration(
[t.ImportDefaultSpecifier(specifier.local)],
t.StringLiteral(`${source.value}/${specifier.imported.name}`)
);
});
if (importDeclaration.length === 1) {
nodePath.replaceWith(importDeclaration[0]);
} else {
nodePath.replaceWithMultiple(importDeclaration);
}
}
},
};
module.exports = function () {
return {
visitor,
};
};
实现自己的 webpack 了解 webpack 的工作流
webpack 编译流程
- 初始化参数:从配置文件和 shell 语句中读取并合并参数,得出最终的配置对象
- 用上一步得到的参数初始化 Compiler 对象
- 加载所有配置的插件
- 执行对象的 run 方法开始执行编译
- 根据配置中的 entry 找出入口文件
- 从入口文件出发,调用所有配置的 Loader 对模块进行编译
- 再找出该模块以来的模块,递归直到所有入口依赖的文件都经过处理
- 根据入口和模块之间的依赖关系,组装成 i 一个个包含多个模块的 chunk
- 再把每个 chunk 转换成一个单独的文件加入到输出列表
- 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
let webpack = require("./webpack"); // 自实现的webpack
const options = require("./webpack.config");
// 1. 初始化参数,从配置文件中读取配置对象,然后和shell参数进行合并得到最终的配置对象(在webpack函数中进行)
// 2. 用上一步得到的参数初始化compiler对象(在webpack函数中进行
// 3. 加载所有配置的插件: 依次调用插件的apply方法,传入compiler对象作为参数(在webpack函数中进行)
let compiler = webpack(options);
// 4. 调用Compiler对象的run方法开始执行编译工作
// 5. 在run方法中根据配置中的entry找到入口文件
compiler.run((err, stats) => {
console.log(err);
console.log(stats.toJson({
entries: true, // 入口信息
modules: true, // 本次打包有哪些模块
chunks: true, // 代码块
assets: true, // 产出的资源
files: true, // 最后生成了哪些文件
}))
})
// webpack.js
// webpack是一个函数,接收options作为参数,返回一个compiler对象
let Compiler = require("./Compiler");
function webpack(options) {
// 1. 初始化参数(options和shell参数)
// 获取shell参数
let shellOptions = process.argv.slice(2).reduce((config, args) => {
let [key, value] = args.split("="); // --mode=production
config[key.slice(2)]=value;
return config;
}, {});
// 合并optons参数和shell参数
let finalOptions = {...options, ...shellOptions};
let compiler = new Compiler(finalOptions);
// 3. 循环加载所有配置的插件
if(finalOptions.plugins && Array.isArray(finalOptions.plugins)) {
for(let plugin of options.plugins) {
plugin.apply(compiler);
}
}
return compiler;
}
module.exports = webpack;
// runPlugin.js
class RunPlugin{
apply(compiler) {
// 注册run这个钩子
compiler.hooks.run.tap('RunPlugin', () => {
console.log("挂载runPlugin")
})
}
}
module.exports = RunPlugin;
// donePlugin.js
class DonePlugin{
apply(compiler) {
// 注册done这个钩子
compiler.hooks.done.tap('DonePlugin', () => {
console.log("挂载donePlugin")
})
}
}
module.exports = DonePlugin;
// selfLoader
// loader就是一个函数,接收原始内容,返回转换后的内容
function selfLoader(source) {
console.log('logger1-loader');
return source + '//logger1'
}
module.exports = selfLoader;
// Compiler.js
let {SyncHook} = require("tapable");
let fs = require("fs");
let path = require("path");
let types = require("babel-types"); // 判断某个节点是否是某种类型,生成某个新的节点
let parser = require("@babel/parser"); // 把源码生成ast语法树 @babel/parser就是babylon
let traverse = require("@babel/traverse").default; // 遍历器,用来遍历语法树
let generator = require("@babel/generator").default; // 生成器,根据语法树重新生成代码
let rootPath = toUnixPath(this.options.context || process.cwd());
class Compiler{
constructor(options) {
this.options = options;
this.hooks = {
run: new SyncHook(), // 开启编译
emit: new SyncHook(), // 写入文件系统
done: new SyncHook(), // 编译工作全部完成
}
// webpack4:数组 webpack5:Set
this.entries = new Set(); // 所有的入口模块
this.modules = new Set(); // 所有的模块
this.chunks = new Set(); // 所有的代码块
this.assets = {}; //存放本次编译要产出的文件
this.files = new Set(); // 存放着本次编译所有的产出的文件名
}
run(callback) {
// 触发注册了的钩子执行
this.hooks.run.call();
// 5. 根据配置中的entry找到入口文件(entry最终都是一个对象[字符串写法相当于语法糖])
let entry = {};
if(typeof this.options.entry === 'string') {
entry.main = this.options.entry;
}else{
entry = this.options.entry;
}
// 开始真正的编译了
// 6. 从入口文件出发,调用所有配置的loader对模块进行编译
// webpack配置文件可能会配置: context: process.cwd()
let rootPath = this.options.context || process.cwd();
for(let entryName in entry) {
let entryPath = toUnixPath(path.join(rootPath, entry[entryName])); // 入口文件的绝对路径
// 开始编译入口文件, 返回一个入口模块
let entryModule = this.buildModule(entryName, entryPath);
this.entries.add(entryModule);
// this.modules.add(entryModule); 可加可不加
// 8. 根据入口和模块之间的依赖关系 组装成一个个包含多个模块的chunk
let chunk = { name: entryName, entryModule, modules: Array.from(this.modules).filter(module => module.name === entryName)};
this.chunks.add(chunk);
}
// 9. 再把每个chunk转换成一个单独的文件加入到处输出列表
// 输出列表就是this.assets对象 key是文件名 值是文件内容
let output = this.options.output;
this.chunks.forEach(chunk => {
let filename = output.filename.replace('[name]', chunk.name);
this.assets[filename] = getSource(chunk);
});
// 10. 生成文件 文件内容写入文件系统
this.hooks.emit.call();
this.files = Object.keys(this.assets); // 文件名数组
for(let file in this.assets) {
let filePath = path.join(output.path,file)
fs.writeFileSync(filePath, this.assets[file]);
}
// 到这里, 编译工作就全部结束了 可以触发done的回调了
this.hooks.done.call();
callback(null, {
toJson: () => ({
entries: this.entries,
chunks: this.chunks,
modules: this.modules,
files: this.files,
assets: this.assets
})
})
}
buildModule(entryName, modulePath) {
// 读取出来此模块的内容
let originalSourceCode = fs.readFileSync(modulePath, 'utf8');
let targetSourceCode = originalSourceCode;
// 调用所有配置的loader对模块进行编译
let rules = this.options.module.rules;
// 得到了本文件模块生效的loader有哪些
let loaders = [];
for(let i = 0; i < rules.length; i++) {
// if(rules[i].test.test(modulePath)){}
if(modulePath.match(rules[i].test)){
loaders = [...loaders, ...rules[i].use]
}
}
for(let i = loaders.length - 1; i>=0; i--) {
targetSourceCode = require(loaders[i])(targetSourceCode);
}
// 7. 再找出该模块依赖的模块, 再递归本步骤直到所有入口依赖的文件都经过本步骤的处理
let moduleId = './' + path.posix.relative(rootPath, modulePath); // 模块的相对路径就是模块id ./src/index.js
let module = { id: moduleId, dependencies: [], name: entryName}; // name是所属代码块的名字
// 把转换后的源码转成抽象语法树
let ast = parser.parse(targetSourceCode, {sourceType: "module"});
traverse(ast, {
CallExpression: (nodePath) => {
let { node } = nodePath;
if(node.callee.name === 'require') { // 说明是require('./xxx'),是依赖的模块
// 要引入模块的相对路径
let moduleName = node.arguments[0].value; './title'
// 为了获取要加载的模块的绝对路径depModulePath 第一步获取当前模块的所在目录
let dirname = path.posix.dirname(modulePath);
let depModulePath = path.posix.join(dirname, moduleName);
// 给依赖模块绝对路径添加后缀
let extensions = this.options.resolve.extensions;
depModulePath = tryExtensions(depModulePath, extensions, moduleName, dirname);
// 依赖模块的模块id
let depModuleId = './' + path.posix.relative(rootPath, depModulePath); // ./src/title.js
node.arguments = [types.stringLiteral(depModuleId)]; // ./title.js => ./src/title.js
// 判断现有的已经编译的modules里有没有这个模块,如果有不用添加依赖了,如果没有则需要添加
let alreadyModuleIds = Array.from(this.modules).map(module => module.id);
if(!alreadyModuleIds.includes(depModuleId)) {
module.dependencies.push(depModulePath);
}
}
}
});
let {code} = generator(ast); // 根据新的语法树生成新的代码
module._source = code; // 此模块的源代码
// 递归编译每个依赖项
module.dependencies.forEach(dependency => {
let depModule = this.buildModule(entryName, dependency);
this.modules.add(depModule);
});
return module;
}
}
// 添加文件后缀的方法
function tryExtensions(
depModulePath, // 拼出来的模块路径 c:/src/title
extensions, // ['.js', '.jsx', '.json']
moduleName, // ./title
dirname, // c:/src
){
// 有可能本身就已经带了后缀,就没有必要加了
extensions.unshift(""); // ['', '.js', '.jsx', '.json']
for(let i =0; i < extensions.length; i++) {
if(fs.existsSync(depModulePath+extensions[i])){
return modulePath+extensions[i];
}
}
// 如果执行到了这,说明都没有匹配上
throw new Error(`module not found,error: can't resolve ${moduleName} in ${dirname}`);
}
function getSource(chunk) {
return `
(() => {
var modules = ({
${
chunk.modules.map(module => `
"${module.id}":
((module) => {
${module._source}
})
`).join(",")
}
});
var cache = {};
function require(moduleId) {
var cacheModule = cache[moduleId];
if(cacheModule !== undefined) {
return cacheModule.exports;
}
var module = cache[moduleId] = {esports: {}};
modules[moduleId](module, module.exports, require);
return module.exports;
}
var exports = {};
(() => {
${chunk.entryModule._source}
})();
})();
`;
}
module.exports = Compiler;
// utils 工具方法
// 1. 统一路径分隔符,\换成/
// window 路径分隔符 \ mac linux 路径分隔符 /,为了统一
function toUnixPath(filePath) {
return filePath.replace(/\\/g, "/");
}