Skip to content

Latest commit

 

History

History
225 lines (157 loc) · 6 KB

1.框架设计的核心要素.md

File metadata and controls

225 lines (157 loc) · 6 KB

框架设计的核心要素

提升用户的开发体验

  1. 必要的警告信息

    合理的警告信息,能让用户更清晰且快速地定位问题。

    const mount = function (el) {
      document.querySelector(el).appendChild(document.createTextNode('Hello world.'))
    }
    
    mount('#not-exists') // Uncaught TypeError: Cannot read properties of null (reading 'appendChild')

    根据此信息,我们可能很难去定位问题出在哪里。所以在设计框架时,需要提供更为有用的信息来帮助用户定位问题。

    const mount = function (el) {
      const container = document.querySelector(el)
    
      if (!container) {
        throw new Error(
          `Target selector "${el}" returned null.`
        )
      }
      container.appendChild(document.createTextNode('Hello world.'))
    }
    
    mount('#not-exists')
  2. 直观的输出内容

    在 Vue.js 3 中,当我们在控制台打印一个 Ref 数据时:

    const count = Vue.ref(0)
        console.log(count) // RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 0, _value: 0}

    这样的数据阅读起来是不友好的。

    所以在 Vue.js 3 的源码中提供了 initCustomFormatter 的函数用于在开发环境初始化自定义的 formatter。

    在 Chrome 浏览器中可以通过 设置 -> 控制台 -> 启用自定义格式设置工具 来开启。

    const count = Vue.ref(0)
        console.log(count) // Ref<0>

控制框架代码的体积

在 Vue.js 3 的源码中,通过一些环境常量来决定某些代码只会在特定的环境中生效:

if (__DEV__ && !res) {
  warn(
  	`Failed to mount app: mount target selector "${container}" returned null.`
  )
}

这里的 __DEV__ 常量实际上是通过 rollup.js 的插件配置来预定义的。

在开发环境的版本中 __DEV__ 会被设置为 true,上面的代码就相当于:

if (true && !res) {
  warn(
  	`Failed to mount app: mount target selector "${container}" returned null.`
  )
}

而在生产版本中 __DEV__ 会被设置为 false,上面的代码等价于:

if (false && !res) {
  warn(
  	`Failed to mount app: mount target selector "${container}" returned null.`
  )
}

我们可以发现,这段代码它的判断条件假,所以它永远不会被执行,这种代码称为 dead code,在构建资源的时候会被移除。

良好的 Tree-Shaking

仅仅通过环境常量的形式来排除 dead code 是不够的。

// utils.js
export function foo (obj) {
  obj && obj.foo
}

export function bar () {
  obj && obj.bar
}

// input.js
import { foo } from 'utils'
foo()

像上面的这种情况,bar() 并未被使用到,那么它就不应该出现在打包后的代码中。要做到这一点,我们就需要 Tree-Shaking。

简单地说,Tree-Shaking 指的就是消除那些永远不会被执行的代码,也就是 dead code。

想要实现 Tree-Shaking,必须要满足一个条件:模块必须是 ESM(ES Module)。因为 Tree-Shaking 依赖 ESM 的静态结构。我们以 rollup.js 为例看看 Tree-Shaking 如何工作:

还是上面的代码,我们通过 rollup.js 进行构建

npx rollup input.js -f esm -o bundle.js

我们可以看到 bundle.js 中并未包含 bar() 函数的内容:

// bundle.js
function foo (obj) {
  obj && obj.foo;
}

foo();

这就说明 Tree-Shaking 起了作用。由于我们并未使用到 bar() 函数的内容,因此它被作为 dead code 删除了。但是我们通过代码可以发现,foo() 函数似乎没什么作用:它仅仅是读取了对象中的值。把这段代码删了也不会对我们的程序产生影响,那么 rollup.js 为什么不把这段代码也作为 dead code 删除掉呢?

我们把 input.js 中的代码改造一下:

import { foo } from './utils'

const log = {
  count: 0
}

const obj = Proxy({}, {
  get (target, prop) {
    log.count++
    return target[prop]
  }
})

foo(obj)

在我们读取 obj 中的某个属性时,会记录它的读取次数。

这也就是 Tree-Shaking 中的第二个关键点——副作用。foo() 函数的调用,会产生副作用,那么就不能将其移除。

会不会产生副作用,我们只有在代码运行的时候才会知道,JavaScript 本身是动态语言,因此想要静态地分析哪些代码是 dead code 是非常困难的。

因此,像 rollup.js 这类工具提供了一个机制,让我们开发明确地告诉构建工具某些代码是不会产生副作用,你可以放心地移除它。我们修改一下 input.js:

import { foo } from './utils'

/*#__PURE__*/ foo()

通过 /*#__PURE__*/ 注释告诉构建工具,我这段代码是纯的,不会产生副作用。此时我们再执行构建命令,会发现 bundle.js 里面是空的,这说明 Tree-Shaking 生效了。

框架应输出什么样的构建产物

我们需要针对不同的运行环境提供不同的构建产物。通过在 rollup.config.js 中配置

  1. iife:script 标签直接引用
  2. esm<script type="module">
  3. cjs:CommonJS
export default {
  input: 'input.js',
  output: {
    file: 'output.js',
    format: 'iife' // 指定模块形式
  }
}

特性开关

一个特性对应一个开关,通过开关的形式来决定是否需要某些代码,从而减小资源的体积。

错误处理

提供统一的错误处理接口,并且让用户可以自行的注册错误处理函数来处理错误:

// utils.js
let handleError = null

export default {
  foo (fn) {
    callWithErrorHandling(fn)
  },
  
  registerErrorHandler (fn) {
    handleError = fn
  }
}

function callWithErrorHandling (fn) {
  try {
    fn && fn()
  } catch (e) {
    handleError(e)
  }
}

良好的 TypeScript 支持

🚀 章节链接