注意这段注释代码 /*#__PURE_*_/,该注释的作用就是用来告诉 rollup 对于 foo() 函数的调用不会产生副作用,你可以放心的对其进行 Tree-Shaking,此时再次执行构建命令并查看 bundle.js 文件你会发现它的内容是空的,这说明 Tree-Shaking 生效了。
基于这个案例大家应该明白的是,在编写框架的时候我们需要合理的使用 /*#__PURE_*_/ 注释,如果你去搜索 Vue 的源码会发现它大量的使用了该注释,例如下面这句:
export const isHTMLTag = /#PURE/ makeMap(HTML_TAGS)
也许你会觉得这会不会对编写代码带来很大的心智负担?其实不会,这是因为通常产生副作用的代码都是模块内函数的顶级调用,什么是顶级调用呢?如下代码所示:
foo() // 顶级调用
function bar() {
foo() // 函数内调用
}
可以看到对于顶级调用来说是可能产生副作用的,但对于函数内调用来说只要函数 bar 没有被调用,那么 foo 函数的调用当然不会产生副作用。因此你会发现在 Vue 的源码中,基本都是在一些顶级调用的函数上使用 /*#__PURE__*/ 注释的。当然该注释不仅仅作用与函数,它可以使用在任何语句上,这个注释也不是只有 rollup 才能识别,webpack 以及压缩工具如 terser 都能识别它。
框架应该输出怎样的构建产物
上文中我们提到 Vue 会为开发环境和生产环境输出不同的包,例如 vue.global.js 用于开发环境,它包含了必要的警告信息,而 vue.global.prod.js 用于生产环境,不包含警告信息。实际上 Vue 的构建产物除了有环境上的区分之外,还会根据使用场景的不同而输出其他形式的产物,这一节我们将讨论这些产物的用途以及在构建阶段如何输出这些产物。
不同类型的产物一定是有对应的需求背景的,因此我们从需求讲起。首先我们希望用户可以直接在 html 页面中使用 <script> 标签引入框架并使用:
为了能够实现这个需求,我们就需要输出一种叫做 IIFE 格式的资源,IIFE 的全称是 Immediately Invoked Function Expression ,即”立即调用的函数表达式“,可以很容易的用 JS 来表达:
(function () {
// …
}())
如上代码所示,这就是一个立即执行的函数表达式。实际上 vue.globale.js 文件就是 IIFE 形式的资源,大家可以看一下它的代码结构:
var Vue = (function(exports){
// …
exports.createApp = createApp;
// …
return exports
}({}))
这样当我们使用 <script> 标签直接引入 vue.global.js 文件后,那么全局变量 Vue 就是可用的了。
在 rollup 中我们可以通过配置 format: 'iife' 来实现输出这种形式的资源:
// rollup.config.js
const config = {
input: ‘input.js’,
output: {
file: ‘output.js’,
format: ‘iife’ // 指定模块形式
}
}
export default config
不过随着技术的发展和浏览器的支持,现在主流浏览器对原生 ESM 模块的支持都不错,所以用户除了能够使用 <script> 标签引用 IIFE 格式的资源外,还可以直接引如 ESM 格式的资源,例如 Vue3 会输出 vue.esm-browser.js 文件,用户可以直接用 <script> 标签引入:
为了输出 ESM 格式的资源就需要我们配置 rollup 的输出格式为:format: 'esm'。
你可能已经注意到了,为什么 vue.esm-browser.js 文件中会有 -browser 字样,其实对于 ESM 格式的资源来说,Vue 还会输出一个 vue.esm-bundler.js 文件,其中 -browser 变成了 -bundler。为什么这么做呢?我们知道无论是 rollup 还是 webpack 在寻找资源时,如果 package.json 中存在 module 字段,那么会优先使用 module 字段指向的资源来代替 main 字段所指向的资源。我们可以打开 Vue 源码中的 packages/vue/package.json 文件看一下:
{
“main”: “index.js”,
“module”: “dist/vue.runtime.esm-bundler.js”,
}
其中 module 字段指向的是 vue.runtime.esm-bundler.js 文件,意思就是说如果你的项目是使用 webpack 构建的,那你使用的 Vue 资源就是 vue.runtime.esm-bundler.js ,也就是说带有 -bundler 字样的 ESM 资源是给 rollup 或 webpack 等打包工具使用的,而带有 -browser 字样的 ESM 资源是直接给 <script type="module"> 去使用的。
那他们之间的区别是什么呢?那这就不得不提到上文中的 __DEV__ 常量,当构建用于 <script> 标签的 ESM 资源时,如果是用于开发环境,那么 __DEV__ 会设置为 true;如果是用于生产环境,那么 __DEV__ 常量会被设置为 false ,从而被 Tree-Shaking 移除。但是当我们构建提供给打包工具的 ESM 格式的资源时,我们不能直接把 __DEV__ 设置为 true 或 false,而是使用 (process.env.NODE_ENV !== 'production') 替换掉 __DEV__ 常量。例如下面的源码:
if (DEV) {
warn(useCssModule() is not supported in the global build.)
}
在带有 -bundler 字样的资源中会变成:
if ((process.env.NODE_ENV !== ‘production’)) {
warn(useCssModule() is not supported in the global build.)
}
这样用户侧的 webpack 配置可以自己决定构建资源的目标环境,但是最终的效果其实是一样的,这段代码也只会出现在开发环境。
用户除了可以直接使用 <script> 标签引入资源,我们还希望用户可以在 Node.js 中通过 require 语句引用资源,例如:
const Vue = require(‘vue’)
为什么会有这种需求呢?答案是服务端渲染,当服务端渲染时 Vue 的代码是运行在 Node.js 环境的,而非浏览器环境,在 Node.js 环境下资源的模块格式应该是 CommonJS ,简称 cjs。为了能够输出 cjs 模块的资源,我们可以修改 rollup 的配置:format: 'cjs' 来实现:
// rollup.config.js
const config = {
input: ‘input.js’,
output: {
file: ‘output.js’,
format: ‘cjs’ // 指定模块形式
}
}
export default config
特性开关
在设计框架时,框架会提供诸多特性(或功能)给用户,例如我们提供 A、B、C 三个特性给用户,同时呢我们还提供了 a、b、c 三个对应的特性开关,用户可以通过设置 a、b、c 为 true 和 false 来代表开启和关闭,那么将会带来很多收益:
-
对于用户关闭的特性,我们可以利用 Tree-Shaking 机制让其不包含在最终的资源中。
-
该机制为框架设计带来了灵活性,可以通过特性开关任意为框架添加新的特性而不用担心用不到这些特性的用户侧资源体积变大,同时当框架升级时,我们也可以通过特性开关来支持遗留的 API,这样新的用户可以选择不适用遗留的 API,从而做到用户侧资源最小化。
那怎么实现特性开关呢?其实很简单,原理和上文提到的 __DEV__ 常量一样,本质是利用 rollup 的预定义常量插件来实现,那一段 Vue3 的 rollup 配置来看:
{
FEATURE_OPTIONS_API: isBundlerESMBuild ? __VUE_OPTIONS_API__ : true,
}
其中 __FEATURE_OPTIONS_API__ 类似于 __DEV__,我们可以在 Vue3 的源码中搜索,可以找到很多类似如下代码这样的判断分支:
// support for 2.x options
if (FEATURE_OPTIONS_API) {
currentInstance = instance
pauseTracking()
applyOptions(instance, Component)
resetTracking()
currentInstance = null
}
当 Vue 构建资源时,如果构建的资源是用于给打包工具使用的话(即带有 -bundler 字样的资源),那么上面代码在资源中会变成:
// support for 2.x options
if (VUE_OPTIONS_API) { // 这一这里
currentInstance = instance
pauseTracking()
applyOptions(instance, Component)
resetTracking()
currentInstance = null
}
其中 __VUE_OPTIONS_API__ 就是一个特性开关,用户侧就可以通过设置 __VUE_OPTIONS_API__ 来控制是否包含这段代码。通常用户可以使用 webpack.DefinePlugin 插件实现:
// webpack.DefinePlugin 插件配置
new webpack.DefinePlugin({
VUE_OPTIONS_API: JSON.stringify(true) // 开启特性
})
最后再来详细解释一下 __VUE_OPTIONS_API__ 开关是干嘛用的,在 Vue2 中我们编写的组件叫做组件选项 API:
export default {
data() {}, // data 选项
computed: {}, // computed 选项
// 其他选项…
}
但是在 Vue3 中,更推荐使用 Composition API 来编写代码,例如:
export default {
setup() {
const count = ref(0)
const doubleCount = computed(() => count.value * 2) // 相当于 Vue2 中的 computed 选项
}
}
但是为了兼容 Vue2,在 Vue3 中仍然可以使用选项 API 的方式编写代码,但是对于明确知道自己不会使用选项 API 的用户来说,它们就可以选择使用 __VUE_OPTIONS_API__ 开关来关闭该特性,这样在打包的时候 Vue 的这部分代码就不会包含在最终的资源中,从而减小资源体积。
错误处理
错误处理是开发框架的过程中非常重要的环节,框架的错误处理做的好坏能够直接决定用户应用程序的健壮性,同时还决定了用户开发应用时处理错误的心智负担。
为了让大家对错误处理的重要性有更加直观的感受,我们从一个小例子说起。假设我们开发了一个工具模块,代码如下:
// utils.js
export default {
foo(fn) {
fn && fn()
}
}
该模块导出一个对象,其中 foo 属性是一个函数,接收一个回调函数作为参数,调用 foo 函数时会执行回调函数,在用户侧使用时:
import utils from ‘utils.js’
utils.foo(() => {
// …
})
大家思考一下如果用户提供的回调函数在执行的时候出错了怎么办?此时有两个办法,其一是让用户自行处理,这需要用户自己去 try...catch:
import utils from ‘utils.js’
utils.foo(() => {
try {
// …
} catch (e) {
// …
}
})
但是这对用户来说是增加了负担,试想一下如果 utils.js 不是仅仅提供了一个 foo 函数,而是提供了几十上百个类似的函数,那么用户在使用的时候就需要逐一添加错误处理程序。
第二种办法是我们代替用户统一处理错误,如下代码所示:
// utils.js
export default {
foo(fn) {
try {
fn && fn()
} catch(e) {/* … */}
},
bar(fn) {
try {
fn && fn()
} catch(e) {/* … */}
},
}
这中办法其实就是我们代替用户编写错误处理程序,实际上我们可以进一步封装错误处理程序为一个函数,假设叫它 callWithErrorHandling:
// utils.js
export default {
foo(fn) {
callWithErrorHandling(fn)
},
bar(fn) {
callWithErrorHandling(fn)
},
}
function callWithErrorHandling(fn) {
try {
fn && fn()
} catch (e) {
console.log(e)
}
}
可以看到代码变得简洁多了,但简洁不是目的,这么做真正的好处是,我们有机会为用户提供统一的错误处理接口,如下代码所示:
// utils.js
let handleError = null
export default {
foo(fn) {
callWithErrorHandling(fn)
},
// 用户可以调用该函数注册统一的错误处理函数
resigterErrorHandler(fn) {
handleError = fn
}
}
function callWithErrorHandling(fn) {
try {
fn && fn()
} catch (e) {
// 捕获到的错误传递给用户的错误处理程序
handleError(e)
}
}
我们提供了 resigterErrorHandler 函数,用户可以使用它注册错误处理程序,然后在 callWithErrorHandling 函数内部捕获到错误时,把错误对象传递给用户注册的错误处理程序。
这样在用户侧的代码就会非常简洁且健壮:
import utils from ‘utils.js’
// 注册错误处理程序
utils.resigterErrorHandler((e) => {
console.log(e)
})
utils.foo(() => {/…/})
utils.bar(() => {/…/})
这时错误处理的能力完全由用户控制,用户既可以选择忽略错误,也可以调用上报程序将错误上报到监控系统。
实际上这就是 Vue 错误处理的原理,你可以在源码中搜索到 callWithErrorHandling 函数,另外在 Vue 中我们也可以注册统一的错误处理函数:
import App from ‘App.vue’
const app = createApp(App)
app.config.errorHandler = () => {
// 错误处理程序
}
良好的 Typescript 类型支持
Typescript 是微软开源的编程语言,简称 TS,它是 JS 的超集能够为 JS 提供类型支持。现在越来越多的人和团队在他们的项目中使用 TS 语言,使用 TS 的好处很多,如代码即文档、编辑器的自动提示、一定程度上能够避免低级 bug、让代码的可维护性更强等等。因此对 TS 类型支持的是否完善也成为评价一个框架的重要指标。
那如何衡量一个框架对 TS 类型支持的好坏呢?这里有一个常见的误区,很多同学以为只要是使用 TS 编写就是对 TS 类型支持的友好,其实使用 TS 编写框架和框架对 TS 类型支持的友好是两件关系不大的事儿。考虑到有的同学可能没有接触过 TS,所以这里不会做深入讨论,我们只举一个简单的例子,如下是使用 TS 编写的函数:
function foo(val: any) {
return val
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。


既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
更多面试题
**《350页前端校招面试题精编解析大全》**内容大纲主要包括 HTML,CSS,前端基础,前端核心,前端进阶,移动端开发,计算机基础,算法与数据结构,项目,职业发展等等

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-9Se3lM9K-1712755746819)]
[外链图片转存中…(img-B2dTdVNK-1712755746819)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
[外链图片转存中…(img-UhRB8xaD-1712755746820)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
更多面试题
**《350页前端校招面试题精编解析大全》**内容大纲主要包括 HTML,CSS,前端基础,前端核心,前端进阶,移动端开发,计算机基础,算法与数据结构,项目,职业发展等等
[外链图片转存中…(img-DklVzZaT-1712755746820)]
本文介绍了Vue框架如何通过注释实现Tree-Shaking优化,区分IIFE和ESM模块,以及如何通过特性开关控制模块内容,包括使用__DEV__常量处理错误和提供良好的TypeScript类型支持。
7668

被折叠的 条评论
为什么被折叠?



