# 工程化
# webpack 常见 loader 和 plugin 有哪些?二者的区别是什么?
# 常见 loader
在 webpack 文档里写了:
Loaders | webpack (opens new window)
你可以记住:
babel-loader
把 JS/TS(ES6) 变成 JS(ES5)ts-loader
把 TS 变成 JS,并提示类型错误markdown-loader
把 markdown 变成 htmlhtml-loader
把 html 变成 JS 字符串sass-loader
把 SASS/SCSS 变成 CSScss-loader
把 CSS 变成 JS 字符串style-loader
把 JS 字符串变成 style 标签postcss-loader
把 CSS 变成更优化的 CSSvue-loader
把单文件组件(SFC)变成 JS 模块file-loader
把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件url-loader
和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去eslint-loader
通过 ESLint 检查 JavaScript 代码规范,并提示类型错误thread-loader
用于多进程打包source-map-loader
加载额外的 Source Map 文件,以方便代码调试
# 常见 plugin
也在 webpack 文档里写了:
Plugins | webpack (opens new window)
你可以记住这些:
UglifyJsPlugin
压缩和混淆代码html-webpack-plugin
用于创建 HTML 页面并自动引入 JS 和 CSSclean-webpack-plugin
用于清理之前打包的残余文件mini-css-extract-plugin
用于将 JS 中的 CSS 抽离成单独的 CSS 文件HotModuleReplacementPlugin
热跟新SplitChunksPlugin
用于代码分包(Code Split)DllPlugin
+DllReferencePlugin
用于避免大依赖被频繁重新打包,大幅降低打包时间CommonsChunkPlugin
提高打包效率,将第三方库和业务代码分开打包DefinePlugin
用于在 webpack config 里添加全局变量copy-webpack-plugin
用于拷贝静态文件到 distwebpack-bundle-analyzer
代码分析;
# 二者的区别
- loader 是文件加载器,让 webpack 拥有加载和解析非 JavaScript 文件的能力
- 功能:能够对文件进行编译、优化、混淆(压缩)等,比如 babel-loader / vue-loader
- 运行时机:在创建最终产物之前运行
- plugin 是 webpack 插件(这句废话也很重要)
- 功能:能实现更多功能,比如定义全局变量、Code Split、加速编译等 (基于事件机制工作,会监听 webpack 打包过程中的某些事件,执行改变输出结果的任务)
- 运行时机:在整个打包过程(以及前后)都能运行
# webpack 怎么写一个 plugin 和 loader
实现一个 webpack loader 和 webpack plugin (opens new window)
# Babel 的原理
查看 AST https://astexplorer.net/
Babel 是 JavaScript 编译器:他能让开发者在开发过程中,直接使用各类方言(如 TS、Flow、JSX)或新的语法特性,而不需要考虑运行环境,因为 Babel 可以做到按需转换为低版本支持的代码;Babel 内部原理是将 JS 代码转换为 AST,对 AST 应用各种插件进行处理,最终输出编译后的 JS 代码。
Babel 编译流程
三大步骤
解析阶段:Babel 默认使用 @babel/parser 将代码转换为 AST。解析一般分为两个阶段:词法分析和语法分析。
- 词法分析:对输入的字符序列做标记化(tokenization)操作。
- 语法分析:处理标记与标记之间的关系,最终形成一颗完整的 AST 结构。
转换阶段:Babel 使用 @babel/traverse 提供的方法对 AST 进行深度优先遍历,调用插件对关注节点的处理函数,按需对 AST 节点进行增删改操作。
生成阶段:Babel 默认使用 @babel/generator 将上一阶段处理后的 AST 转换为代码字符串。
# webpack 如何解决开发时的跨域问题?
在开发时,我们的页面在 localhost:8080
,JS 直接访问后端接口(如 https://xiedaimala.com
或 http://localhost:3000
)会报跨域错误。
为了解决这个问题,可以在 webpack.config.js 中添加如下配置:
module.exports = {
//...
devServer: {
proxy: {
'/api': {
target: 'http://xiedaimala.com',
changeOrigin: true
}
}
}
}
此时,在 JS 中请求 /api/users
就会自动被代理到 http://xiedaimala.com/api/users
。
如果希望请求中的 Origin 从 8080 修改为 xiedaimala.com,可以添加 changeOrigin: true
。
如果要访问的是 HTTPS API,那么就需要配置 HTTPS 证书,否则会报错。
不过,如果在 target 下面添加 secure: false
,就可以不配置证书且忽略 HTTPS 报错。
# 工作原理
proxy
工作原理实质上是利用http-proxy-middleware
这个http
代理中间件,实现请求转发给其他服务器
举个例子:
在开发阶段,本地地址为http://localhost:3000
,该浏览器发送一个前缀带有/api
标识的请求到服务端获取数据,但响应这个请求的服务器只是将请求转发到另一台服务器中
const express = require('express')
const proxy = require('http-proxy-middleware')
const app = express()
app.use('/api', proxy({ target: 'http://www.example.org', changeOrigin: true }))
app.listen(3000)
// http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar
# 跨域
在开发阶段, webpack-dev-server
会启动一个本地开发服务器,所以我们的应用在开发阶段是独立运行在 localhost
的一个端口上,而后端服务又是运行在另外一个地址上
所以在开发阶段中,由于浏览器同源策略的原因,当本地访问后端就会出现跨域请求的问题
通过设置webpack proxy
实现代理请求后,相当于浏览器与服务端中添加一个代理者
当本地发送请求的时候,代理服务器响应该请求,并将请求转发到目标服务器,目标服务器响应数据后再将数据返回给代理服务器,最终再由代理服务器将数据响应给本地
在代理服务器传递数据给本地浏览器的过程中,两者同源,并不存在跨域行为,这时候浏览器就能正常接收数据
注意:服务器与服务器之间请求数据并不会存在跨域行为,跨域行为是浏览器安全策略限制
参考文献
- https://vue3js.cn/interview/webpack/proxy.htm
- https://webpack.docschina.org/configuration/dev-server/#devserverproxy
# webpack 如何实现 tree-shaking?
Tree Shaking | webpack (opens new window)
Tree Shaking | webpack 中文文档 (opens new window)
# 是什么
tree-shaking 就是让没有用到的 JS 代码不打包,以减小包的体积。
# 怎么做
怎么删
- 使用 ES Modules 语法(即 ES6 的 import 和 export 关键字)
- CommonJS 语法无法 tree-shaking(即 require 和 exports 语法)
- 需要给 bebel-loader 添加
modules: false
选项(避免 babel 将 js 自动编译为 CommonJS )
- 需要给 bebel-loader 添加
- 引入的时候只引用需要的模块
- 要写
import {cloneDeep} from 'lodash-es'
因为方便 tree-shaking - 不要写
import _ from 'lodash'
因为会导致无法 tree-shaking 无用模块
- 要写
怎么不删:在 package.json 中配置
sideEffects
,防止某些文件被删掉- 比如我 import 了 x.js,而 x.js 只是添加了
window.x
属性,那么 x.js 就要放到 sideEffects 里 - 比如所有被 import 的 CSS 都要放在 sideEffects 里
- 比如我 import 了 x.js,而 x.js 只是添加了
怎么开启:在 webpack config 中将 mode 设置为 production(开发环境没必要 tree-shaking)
mode: production
给 webpack 加了非常多优化 (opens new window)。
原理: tree-shaking 通过静态分析,对代码进行扫描分析,经过词法分析、语法分析、AST 树等分析手段,识别未被引用的代码,标记为冗余代码,真正剔除操作一般是 uglifyjs 模块。
# webpack 热更新原理
webpack 在热更新模式下,启动服务后,服务端会与客户端建立一个长连接(websocket)。文件修改后,服务端会通过长链接向客户端推送一条消息,客户端收到后,会重新请求一个 js 文件,返回的 js 文件会调用 webpackHotUpdatehmr 方法,用于替换掉 webpack_modules 中的部分代码。
- webpack-dev-server 启动本地服务,与 Client 建立长连接
- Webpack 监听文件修改,修改后通过长连接通知客户端;
- Client 重新请求文件,替换 webpack_modules 中对应部分
首先,介绍 webpack-dev-server:
webpack-dev-server 主要包含了三个部分:
1.webpack: 负责编译代码
2.webpack-dev-middleware: 主要负责构建内存文件系统,把 webpack 的 OutputFileSystem 替换成 InMemoryFileSystem。同时作为 Express 的中间件拦截请求,从内存文件系统中把结果拿出来。
3.express:负责搭建请求路由服务。
其次,介绍工作流程:
1.启动 dev-server,webpack 开始构建,在编译期间会向 entry 文件注入热更新代码;
2.Client 首次打开后,Server 和 Client 基于 Socket 建立通讯渠道;
3.修改文件,Server 端监听文件发送变动,webpack 开始编译,直到编译完成会触发"Done"事件;
4.Server 通过 socket 发送消息告知 Client;
5.Client 根据 Server 的消息(hash 值和 state 状态),通过 ajax 请求获取 Server 的 manifest 描述文件;
6.Client 对比当前 modules tree ,再次发请求到 Server 端获取新的 JS 模块;
7.Client 获取到新的 JS 模块后,会更新 modules tree 并替换掉现有的模块;
8.最后调用 module.hot.accept() 完成热更新;
# 如何利用 webpack 来优化前端性能
webpack 做性能优化主要是考虑打包体积和打包速度。
打包体积分析用 webpack-bundle-analyzer
插件,速度分析用:speed-measure-webpack-plugin
插件。
# 如何提高 webpack 构建速度?
构建性能 | webpack 中文文档 (opens new window)
- speed-measure-webpack-plugin 分析构建时间
- externals 拆包采用 cdn 方式引入 vue, vue-router 及组件库
- 使用 DllPlugin 将不常变化的 node_modules 代码提前打包,并复用,如 echarts 下选用的模块
- 使用 thread-loader 进行多线程打包,加速耗时的 loader (例如 babel-loader)
- 处于开发环境时,在 webpack config 中 使用 cache-loader 加速 Vue/Babel/TypeScript 编译开启缓存
- import 优化,运用这个插件(babel-plugin-dynamic-import-node)能在代码使用了 import 语法的情况下,大大提高代码的编译速度。
- 处于生产环境时,关闭不必要的环节,比如可以关闭 source map
- 网传的 HardSourceWebpackPlugin 已经一年多没更新了,谨慎使用
# webpack 能做哪些性能优化
- 压缩代码
- tree-shaking
- 根据文件内容生成 hash 当作文件名,配合 CDN 做文件缓存
- 分割代码,按需加载
- 将第三方插件或公共代码单独提取出来打包
三十分钟掌握 Webpack 性能优化 (opens new window)
# webpack 模块加载原理
深入了解 webpack 模块加载原理 (opens new window)
# webpack 的构建流程是什么
webpack 构建流程分析 (opens new window)
# webpack 的分包策略
webpack 的分包策略 (opens new window)
# webpack 与 vite 的区别是什么?
开发环境区别
vite 自己实现 server,不对代码打包,充分利用浏览器对
<script type=module>
的支持假设 main.js 引入了 vue
该 server 会把
import { createApp } from 'vue'
改为import { createApp } from "/node_modules/.vite/vue.js"
这样浏览器就知道去哪里找 vue.js 了
webpack-dev-server 常使用 babel-loader 基于内存打包,比 vite 慢很多很多很多
- 该 server 会把 vue.js 的代码(递归地)打包进 main.js
生产环境区别
vite 使用 rollup + esbuild 来打包 JS 代码
webpack 使用 babel 来打包 JS 代码,比 esbuild 慢很多很多很多
- webpack 能使用 esbuild 吗?可以,你要自己配置(很麻烦)。
文件处理时机
vite 只会在你请求某个文件的时候处理该文件
webpack 会提前打包好 main.js,等你请求的时候直接输出打包好的 JS 给你
目前已知 vite 的缺点有:
热更新常常失败,原因不清楚(F5, ctrl + R)
有些功能 rollup 不支持,需要自己写 rollup 插件
不支持非现代浏览器(浏览器需要支持
<script type=module>
)
# webpack 和 rollup 的区别
rollup 从设计之初就是面向 ES module 的,它诞生时 AMD、CMD、UMD 的格式之争还很火热,作者希望充分利用 ES module 机制,构建出结构扁平,性能出众的类库.
// rollup.config.js
import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import babel from 'rollup-plugin-babel'
export default [
{
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'umd',
name: 'test'
},
plugins: [
resolve(),
commonjs(),
babel({
exclude: 'node_modules/**'
})
]
}
]
webpack 致力于复杂 SPA 的模块化构建,优势在于:
- 通过 loader 处理各种各样的资源依赖
- HMR 模块热替换
- 代码按需加载
- 提取公共模块
rollup 致力于打造性能出众的类库,有如下优势:
- 编译出来的代码
可读性好
- rollup 打包后生成的 bundle 内容十分
干净
,没有什么多余的代码。相比 webpack(webpack 打包后会生成webpack_require等 runtime 代码),rollup 拥有无可比拟的性能优势,这是由依赖处理方式决定的,编译时依赖处理(rollup)自然比运行时依赖处理(webpack)性能更好
- 对于 ES 模块依赖库,rollup 会静态分析代码中的 import,并将排除任何未实际使用的代码
- 支持程序流分析,能更加正确的判断项目本身的代码是否有副作用(配合 tree-shaking)
- 支持导出
es
模块文件(webpack 不支持导出 es 模块)
参考资料:
- 【第九期】Rollup:下一代 ES 模块打包工具 (opens new window)
- http://www.caoyuanpeng.com/Webpack/rollup跟webpack打包的区别.html
# webpack 怎么配置多页应用?
这是对应的 webpack config:
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js'
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
chunks: ['app']
}),
new HtmlWebpackPlugin({
filename: 'admin.html',
chunks: ['admin']
})
]
}
但是,这样配置会有一个「重复打包」的问题:假设 app.js 和 admin.js 都引入了 vue.js,那么 vue.js 的代码既会打包进 app.js,也会打包进 admin.js。我们需要使用 optimization.splitChunks
将共同依赖单独打包成 common.js(HtmlWebpackPlugin 会自动引入 common.js)。
# webpack 如何支持无限多页面呢?
写点 Node.js 代码不就实现了么?
const HtmlWebpackPlugin = require('html-webpack-plugin')
const fs = require('fs')
const path = require('path')
const filenames = fs
.readdirSync('./src/pages')
.filter(file => file.endsWith('.js'))
.map(file => path.basename(file, '.js'))
const entries = filenames.reduce((result, name) => ({ ...result, [name]: `./src/pages/${name}.js` }), {})
const plugins = filenames.map(
name =>
new HtmlWebpackPlugin({
filename: name + '.html',
chunks: [name]
})
)
module.exports = {
entry: {
...entries
},
plugins: [...plugins]
}
# swc、esbuild 是什么?
# swc
实现语言:Rust
功能:编译 JS/TS、打包 JS/TS
优势:比 babel 快很多很多很多(20 倍以上)
能否集成进 webpack:能
使用者:Next.js、Parcel、Deno、Vercel、ByteDance、Tencent、Shopify……
做不到:
对 TS 代码进行类型检查(用 tsc 可以)
打包 CSS、SVG
# esbuild
实现语言:Go
功能:同上
优势:比 babel 快很多很多很多很多很多很多(10~100 倍)
能否集成进 webpack:能
使用者:vite、vuepress、snowpack、umijs、blitz.js 等
做不到:
对 TS 代码进行类型检查
打包 CSS、SVG
# 谈谈对于工程化的理解
前端工程化,其实是软件工程在前端方面的应用。什么是软件工程?百度百科的定义:
软件工程是一门研究用工程化方法构建和维护有效的、使用的和高质量的软件的学科。
换句话说,工程化的目的就是为了提升团队的开发效率。例如大家所熟悉的构建打包、性能优化、自动化部署等知识,都属于工程化的内容。
# 重要的几个概念
1、模块化:JS 的模块化、css 的模块化、资源的模块化(模块化是在文件层面上,对代码或资源的拆分)
2、组件化:组件化是在设计层面上,对 UI(用户界面)的拆分,组件化更重要是一种分治思想,封装组件需要注意组件之间的依赖关系(继承、扩展、嵌套)
3、规范化:编码规范、前后端接口规范、文档规范、组件规范、git 分支管理、commit 描述规范...
4、自动化:雪碧图、持续集成、自动化构建、自动化部署、自动化测试(任何简单机械的重复劳动都应该让机器去完成)
# 目录
- 技术选型:如何进行技术选型?
- 统一规范
- 模块化、组件化
- 构建工具
- 自动化
- 性能优化
- 监控
- 重构
- 项目拆分
- 测试
- 创造性
参考
- yanyue404 - 对前端工程化的理解 (opens new window)
- https://woai3c.gitee.io/introduction-to-front-end-engineering/
# 性能优化
# 如何在 H5 和小程序项目中计算白屏时间和首屏时间
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/272
# 前端项目如何找出性能瓶颈
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/300
# 前端基建可以做哪些事情?
https://juejin.cn/post/7256879435339628604
来自 ChatGPT 的回答-~~ ⬇️ ⬇️ ⬇️
前端基建是指在前端开发过程中,为提高开发效率、代码质量和团队协作而构建的一些基础设施和工具。下面是前端基建可以做的一些事情:
脚手架工具
:开发和维护一个通用的脚手架工具,可以帮助团队快速初始化项目结构、配置构建工具、集成常用的开发依赖等。组件库
:开发和维护一个内部的组件库,包含常用的 UI 组件、业务组件等,提供给团队成员复用,减少重复开发的工作量。构建工具和打包工具
:搭建和维护一套完善的构建和打包工具链,包括使用 Webpack、Parcel 等工具进行代码的压缩、合并、打包等工具,优化前端资源加载和性能。自动化测试工具
:引入自动化测试工具,如 Jest、Mocha 等,编写和维护测试用例,进行单元测试、集成测试、UI 测试等,提高代码质量和可靠性。文档工具
:使用工具如 JSDoc、Swagger 等,生成项目的 API 文档、接口文档等,方便团队成员查阅和维护。Git工作流
:制定和规范团队的 Git 工作流程,使用版本控制工具管理代码,方便团队协作和代码回退。性能监控和优化
:引入性能监控工具,如 Lighthouse、Web Vitals 等,对项目进行性能分析,优化网页加载速度、响应时间等。工程化规范
:制定并推广团队的代码规范、目录结构规范等,提高代码的可读性、可维护性和可扩展性。持续集成和部署
:搭建持续集成和部署系统,如 Jenkins、Travis CI 等,实现代码的自动构建、测试和部署,提高开发效率和代码质量。项目文档和知识库
:建立一个内部的项目文档和知识库,记录项目的技术细节、开发经验、常见问题等,方便团队成员查阅和学习。
通过建立和维护前端基建,可以提高团队的协作效率,减少重复劳动,提高代码质量和项目的可维护性。
当涉及到前端基建时,还有一些其他的事情可以考虑:
代码质量工具
:引入代码质量工具,如 ESLint、Prettier 等,对代码进行静态分析和格式化,提高代码的一致性和可读性。国际化支持
:为项目添加国际化支持,可以通过引入国际化库,如 i18next、vue-i18n 等,实现多语言的切换和管理。错误监控和日志收集
:引入错误监控工具,如 Sentry、Bugsnag 等,实时监控前端错误,并收集错误日志,方便进行问题排查和修复。前端性能优化工具
:使用工具如 WebPageTest、Chrome DevTools 等,对项目进行性能分析和优化,提高页面加载速度、响应时间等。缓存管理
:考虑合理利用浏览器缓存和服务端缓存,减少网络请求,提升用户访问速度和体验。移动端适配
:针对移动端设备,采用响应式设计或使用 CSS 媒体查询等技术,实现移动端适配,保证页面在不同尺寸的设备上有良好的显示效果。安全防护
:对项目进行安全审计,使用安全防护工具,如 CSP(Content Security Policy)、XSS 过滤等,保护网站免受常见的安全攻击。性能优化指标监控
:监控和分析关键的性能指标,如页面加载时间、首次渲染时间、交互响应时间等,以便及时发现和解决性能问题。前端日志分析
:使用日志分析工具,如 ELK(Elasticsearch、Logstash、Kibana)等,对前端日志进行收集和分析,了解用户行为和页面异常情况。跨平台开发
:考虑使用跨平台开发框架,如 React Native、Flutter 等,实现一套代码在多个平台上复用,提高开发效率。编辑器配置和插件
:为团队提供统一的编辑器配置文件,包括代码格式化、语法高亮、代码自动补全等,并推荐常用的编辑器插件,提高开发效率。文档生成工具
:使用工具如 Docusaurus、VuePress 等,为项目生成漂亮的文档网站,方便团队成员查阅和维护项目文档。Mock数据和接口管理
:搭建一个 Mock 服务器,用于模拟后端接口数据,方便前端开发和测试,同时可以考虑使用接口管理工具,如 Swagger 等,方便接口的定义和调试。前端监控和统计
:引入前端监控工具,如 Google Analytics、百度统计等,收集用户访问数据和行为信息,用于分析和优化用户体验。移动端调试工具
:使用工具如 Eruda、VConsole 等,帮助在移动端设备上进行调试和错误排查,提高开发效率。自动化部署
:配置自动化部署流程,将项目的代码自动部署到服务器或云平台,减少人工操作,提高发布效率和稳定性。前端团队协作工具
:使用团队协作工具,如 GitLab、Bitbucket 等,提供代码托管、项目管理、任务分配和团队沟通等功能,增强团队协作效率。前端培训和知识分享
:组织定期的前端培训和技术分享会,让团队成员相互学习和交流,推动技术的共享和提升。客户端性能优化
:针对移动端应用,可以使用工具如 React Native Performance、Weex 等,进行客户端性能优化,提高应用的响应速度和流畅度。技术选型和评估
:定期评估和研究前端技术的发展趋势,选择适用的技术栈和框架,以保持项目的竞争力和可持续发展。
以上是一些可以考虑的前端基建事项,根据项目需求和团队情况,可以选择适合的工具和技术进行实施。同时,持续关注前端领域的最新技术和工具,不断优化和改进前端基建,以提高开发效率和项目质量。
当涉及到前端基建时,还有一些其他的事情可以考虑:
统一的状态管理
:引入状态管理工具,如 Redux、Vuex 等,帮助团队管理前端应用的状态,提高代码的可维护性和可扩展性。前端日志记录
:引入前端日志记录工具,如 log4javascript、logrocket 等,记录前端应用的运行日志,方便排查和解决问题。前端代码扫描
:使用静态代码扫描工具,如 SonarQube、CodeClimate 等,对前端代码进行扫描和分析,发现潜在的问题和漏洞。前端数据可视化
:使用数据可视化工具,如 ECharts、Chart.js 等,将数据以图表或图形的形式展示,增强数据的可理解性和可视化效果。前端容灾和故障处理
:制定容灾方案和故障处理流程,对前端应用进行监控和预警,及时处理和恢复故障,提高系统的可靠性和稳定性。前端安全加固
:对前端应用进行安全加固,如防止 XSS 攻击、CSRF 攻击、数据加密等,保护用户数据的安全性和隐私。前端版本管理
:建立前端代码的版本管理机制,使用工具如 Git、SVN 等,管理和追踪代码的变更,方便团队成员之间的协作和版本控制。前端数据缓存
:考虑使用 Local Storage、Session Storage 等技术,对一些频繁使用的数据进行缓存,提高应用的性能和用户体验。前端代码分割
:使用代码分割技术,如 Webpack 的动态导入(Dynamic Import),将代码按需加载,减少初始加载的资源大小,提高页面加载速度。前端性能监测工具
:使用性能监测工具,如 WebPageTest、GTmetrix 等,监测前端应用的性能指标,如页面加载时间、资源加载时间等,进行性能优化。
以上是一些可以考虑的前端基建事项,根据项目需求和团队情况,可以选择适合的工具和技术进行实施。同时,持续关注前端领域的最新技术和工具,不断优化和改进前端基建,以提高开发效率和项目质量。
# Monorepo vs Multirepo
Monorepo 的全称是 monolithic repository,即单体式仓库,与之对应的是 Multirepo(multiple repository),这里的"单"和"多"是指每个仓库中所管理的模块数量。
Multirepo 是比较传统的做法,即每一个 package 都单独用一个仓库来进行管理。例如:Rollup, ...
Monorep 是把所有相关的 package 都放在一个仓库里进行管理,每个 package 独立发布。 例如:React, Angular, Babel, Jest, Umijs, Vue ...
一图胜千言:
当然到底哪一种管理方式更好,仁者见仁,智者见智。前者允许多元化发展(各项目可以有自己的构建工具、依赖管理策略、单元测试方法),后者希望集中管理,减少项目间的差异带来的沟通成本。
虽然拆分子仓库、拆分子 npm 包是进行项目隔离的天然方案,但当仓库内容出现关联时,没有任何一种调试方式比源码放在一起更高效。
结合我们项目的实际场景和业务需要,天然的 MonoRepo ! 因为工程化的最终目的是让业务开发可以 100% 聚焦在业务逻辑上,那么这不仅仅是脚手架、框架需要从自动化、设计上解决的问题,这涉及到仓库管理的设计。
一个理想的开发环境可以抽象成这样:
"只关心业务代码,可以直接跨业务复用而不关心复用方式,调试时所有代码都在源码中。"
在前端开发环境中,多 Git Repo,多 npm 则是这个理想的阻力,它们导致复用要关心版本号,调试需要 npm link。而这些是 MonoRepo 最大的优势。
上图中提到的利用相关工具就是今天的主角 Lerna ! Lerna 是业界知名度最高的 Monorepo 管理工具,功能完整。
单一仓库如何产出不同项目?
简要思路:
项目目录划分:在 packages 目录下管理需要复用的代码,如(utils 公共函数、components 公共组件、templates 公共模板、store 数据流层等),prd 目录下划分不同项目的页面文件夹
构建参数设计:区分运行环境、执行的 prd 子项目和配置文件(如 vue.config.js)
正确运行:通过 spawnSync 等进程执行工具运行 prd 子项目
# pnpm
https://pnpm.io/zh/
pnpm 是新一代快速的,节省磁盘空间的包管理工具
- 快速: pnpm is up to 2x faster than npm
- 高效: node_modules 中的文件为复制或链接自特定的内容寻址存储库
- 严格: pnpm 默认创建了一个非平铺的 node_modules,因此代码无法访问任意包
支持 monorepos :pnpm 内置支持单仓多包
pnpm 内置了对单一存储库(也称为多包存储库、多项目存储库或单体存储库)的支持, 你可以创建一个 workspace (opens new window) 以将多个项目合并到一个仓库中。
pnpm 版本兼容性: https://pnpm.io/zh/installation#%E5%85%BC%E5%AE%B9%E6%80%A7
pnpm 的命令,以下是简便的 npm 命令等效列表:
npm 命令 | pnpm 等效 |
---|---|
npm install | pnpm install (opens new window) |
npm i <pkg> | pnpm add <pkg> (opens new window) |
npm run <cmd> | pnpm <cmd> (opens new window) |
npm uninstall <cmd> | pnpm remove <cmd> (opens new window) |
# 谈谈微前端
微前端就是微服务在前端的应用,也就是前端微服务.
# 如果使用微服务来重构整个应用有什么好处?
一个应用分解成多个服务,每个服务独自服务内部的功能。例如原来的应用有 abcd 四个页面,现在分解成两个服务,第一个服务有 ab 两个页面,第二个服务有 cd 两个页面,组合在一起就和原来的应用一样。
当应用其中一个服务出故障时,其他服务仍可以正常访问。例如第一个服务出故障了, ab 页面将无法访问,但 cd 页面仍能正常访问。
好处:不同的服务独立运行,服务与服务之间解耦。我们可以把服务理解成组件,就像本小书第 3 章《前端组件化》中所说的一样。每个服务可以独自管理,修改一个服务不影响整体应用的运行,只影响该服务提供的功能。
另外在开发时也可以快速的添加、删除功能。例如电商网站,在不同的节假日时推出的活动页面,活动过后马上就可以删掉。
难点:不容易确认服务的边界。当一个应用功能太多时,往往多个功能点之间的关联会比较深。因而就很难确定这一个功能应该归属于哪个服务。
- https://woai3c.github.io/introduction-to-front-end-engineering/11.html
现在我们将使用微前端框架 qiankun (opens new window)来构建一个微前端应用。之所以选用 qiankun 框架,是因为它有以下几个优点:
技术栈无关,任何技术栈的应用都能接入。
样式隔离,子应用之间的样式互不干扰。
子应用的 JavaScript 作用域互相隔离。
资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
# 前端监控
https://woai3c.github.io/introduction-to-front-end-engineering/07.html
# 什么时候需要监控
- 当你的应用频繁报错找不到原因的时候。(集成性能监控的探针 sdk, 收集错误信息)
- 需要分析用户兴趣爱好、购买习惯。 (集成神策埋点, 收集自定义事件, 通过 PV UV 用户点击,页面跳转等事件漏斗进行统计分析)
- 需要优化程序的时候,可以做监控收集数据,做针对性的优化。(集成性能监控的探针 sdk, 性能分析)
- 需要保证服务可靠性稳定性。(集成性能监控的探针 sdk)
如果你的应用符合以上任意一条,就可以对应用实行监控了。监控的作用有两个:事前预警和事后分析。
事前预警:提前设置一个阈值,当监控的数据达到阈值时,通过短信或者邮件通知管理员。例如 API 请求数量突然间暴涨,就得进行报警,否则可能会造成服务器宕机。
事后分析:通过监控日志文件,分析故障原因和故障发生点。从而做出修改,防止这种情况再次发生。
本章内容分为前端监控原理分析和如何对项目实行监控两个部分。第一部分是讲如何写一个简易的监控 SDK,第二部分是讲如何使用 sentry (opens new window)实现项目监控。
好了,下面让我们开始进入正文吧。
一个完整的前端监控平台包括三个部分:数据采集与上报、数据整理和存储、数据展示。
本章要讲的就是其中的第一个环节------数据采集与上报。下图是本章要讲述内容的大纲,大家可以先大致了解一下:
仅看理论知识是比较难以理解的,为此我结合本章要讲的技术要点写了一个简单的监控 SDK (opens new window),可以用它来写一些简单的 DEMO,帮助加深理解。再结合本章一起阅读,效果更好。
# 性能数据采集
chrome 开发团队提出了一系列用于检测网页性能的指标:
- FP(first-paint),从页面加载开始到第一个像素绘制到屏幕上的时间
- FCP(first-contentful-paint),从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间
- LCP(largest-contentful-paint),从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间
- CLS(layout-shift),从页面加载开始和其生命周期状态 (opens new window)变为隐藏期间发生的所有意外布局偏移的累积分数
这四个性能指标都需要通过 PerformanceObserver (opens new window)来获取(也可以通过 performance.getEntriesByName()
获取,但它不是在事件触发时通知的)。PerformanceObserver 是一个性能监测对象,用于监测性能度量事件。
# 错误数据采集
- 资源加载错误: 使用 addEventListener() 监听 error 事件,可以捕获到资源加载失败错误。
- js 错误: 使用 window.onerror 可以监听 js 错误。
- promise 错误: 使用 addEventListener() 监听 unhandledrejection 事件,可以捕获到未处理的 promise 错误。
- 生产报错信息还原: 使用 source-map.
- Vue 错误: 利用 window.onerror 是捕获不到 Vue 错误的,它需要使用 Vue 提供的 API 进行监听。
Vue.config.errorHandler
# 数据上报
上报方法
数据上报可以使用以下几种方式:
我写的简易 SDK 采用的是第一、第二种方式相结合的方式进行上报。利用 sendBeacon 来进行上报的优势非常明显。
使用
sendBeacon()
方法会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。
在不支持 sendBeacon 的浏览器下我们可以使用 XMLHttpRequest 来进行上报。一个 HTTP 请求包含发送和接收两个步骤。其实对于上报来说,我们只要确保能发出去就可以了。也就是发送成功了就行,接不接收响应无所谓。为此,我做了个实验,在 beforeunload 用 XMLHttpRequest 传送了 30kb 的数据(一般的待上报数据很少会有这么大),换了不同的浏览器,都可以成功发出去。当然,这和硬件性能、网络状态也是有关联的。
上报时机
上报时机有三种:
- 采用
requestIdleCallback/setTimeout
延时上报。 - 在
beforeunload()
回调函数里上报。 - 缓存上报数据,达到一定数量后再上报。
建议将三种方式结合一起上报:
- 先缓存上报数据,缓存到一定数量后,利用
requestIdleCallback/setTimeout
延时上报。 - 在页面离开时统一将未上报的数据进行上报。
上报时机代码参考
- https://github.com/woai3c/monitor-demo/blob/main/src/utils/report.js
import { originalOpen, originalSend } from './xhr'
import { addCache, getCache, clearCache } from './cache'
import generateUniqueID from '../utils/generateUniqueID'
import config from '../config'
export function isSupportSendBeacon() {
return !!window.navigator?.sendBeacon
}
const sendBeacon = isSupportSendBeacon() ? window.navigator.sendBeacon.bind(window.navigator) : reportWithXHR
const sessionID = generateUniqueID()
export function report(data, isImmediate = false) {
if (!config.url) {
console.error('请设置上传 url 地址')
}
const reportData = JSON.stringify({
id: sessionID,
appID: config.appID,
userID: config.userID,
data
})
if (isImmediate) {
sendBeacon(config.url, reportData)
return
}
if (window.requestIdleCallback) {
window.requestIdleCallback(
() => {
sendBeacon(config.url, reportData)
},
{ timeout: 3000 }
)
} else {
setTimeout(() => {
sendBeacon(config.url, reportData)
})
}
}
let timer = null
export function lazyReportCache(data, timeout = 3000) {
addCache(data)
clearTimeout(timer)
timer = setTimeout(() => {
const data = getCache()
if (data.length) {
report(data)
clearCache()
}
}, timeout)
}
export function reportWithXHR(data) {
const xhr = new XMLHttpRequest()
originalOpen.call(xhr, 'post', config.url)
originalSend.call(xhr, JSON.stringify(data))
}
# 前端监控部署
前面说的都是监控原理,但要实现还是得自己动手写代码。为了避免麻烦,我们可以用现有的工具 sentry (opens new window)去做这件事。
sentry 是一个用 python 写的性能和错误监控工具,你可以使用 sentry 提供的服务(免费功能少),也可以自己部署服务.