从Vue学封装前端库的必备细节(二)

106 阅读1分钟

文章基于对《Vue.js 设计与实现》「框架设计的核心要素」章节学习,外加个人理解发散,并未完全照搬书中代码和文字。推荐购买正版《Vue.js 设计与实现》进行深度学习。

控制代码体积

上面的代码中,我们在必要的地方加上了警告提示,但这些提示在生产环境其实并没有太大意义,因为普通用户并不关心。

而库的大小也是衡量一个库的标准之一,同样的功能,代码体积越小,请求加载越快。

所以这就意味着我们需要在生产环境中移除掉这些警告提示用的代码,这里我们可以通过一个常量来进行控制:

import {isCanvas} from './utils'

class Poster {
    constructor() {}
    mountCanvas(canvas, el) {
        const el = document.querySelector(selector)
        if (__DEV__ && !el) {
            warn(`${el} 元素不存在 ${__DEV__}`)
        }
        if (__DEV__ && isCanvas()) {
            warn(`${el} `)
        }
        (el || document.body).appendChild(canvas)
    }
}

上面我们定义了一个常量 __DEV__,这个常量可以在通过 rollup 打包时进行替换。

安装 rollup 插件 @rollup/plugin-replace

yarn add @rollup/plugin-replace -D

修改 rollup 配置:

import replace from '@rollup/plugin-replace';

const NodeEnv = {
    dev: 'development',
    prod: 'production',
}
export default {
    input: './src/index.js',
    output: {
        file: './dist/index.js',
        format: 'esm'
    },
    plugins: [
      replace({
        '__DEV__': process.env.NODE_ENV === NodeEnv.dev,
      })
    ]
}

修改 package.json 的 scripts 命令:

  "scripts": {
    "dev": "rollup -c --environment NODE_ENV:development",
    "build": "rollup -c --environment NODE_ENV:production"
  },

执行 yarn dev,常量 __DEV__ 的值为 true,查看构建好的文件:

rollup-replace-1.png

此时 if 条件语句里,警告用的代码存在。

执行 yarn build,常量 __DEV__ 的值为 false,打开构建好的文件:

rollup-replace-2.png

由于 if 条件为 false,所以条件语句内警告用的代码,在打包时就被 rollup 剔除了。

特性开关

一个成熟稳定的框架往往需要向后做一定的兼容,但假如开发过程中完全使用新语法,那框架里处理兼容的代码是不需要的。

所以我们就需要一个控制开关,别人在使用框架时就可以自行控制打包时是否需要保留这些特性。

实现这种控制开关的原理和上面的常量 __DEV__ 类似,不过在构建框架的时候不做处理,而是暴露给使用者自己控制。

比如 Vue3 依然支持 Vue2 的 optional 语法:

// ...
// support for 2.x options
if (__VUE_OPTIONS_API__ && !(false )) {
    setCurrentInstance(instance);
    pauseTracking();
    applyOptions(instance);
    resetTracking();
    unsetCurrentInstance();
}
// ...

上面代码中 __VUE_OPTIONS_API__ 常量就是一个特性开关,如果我们的项目完全使用 composition api,那这些兼容用的代码其实是不需要的。

如果使用 webpack 构建项目,我们可以通过如下配置删除代码:

// ...
plugins: [
    // Define Bundler Build Feature Flags
    new webpack.DefinePlugin({
        // Drop Options API from bundle
        __VUE_OPTIONS_API__: false,
    }),
}

如果使用 vite,则可以在 vite.config.js 里配置:

// ...
define: {
    __VUE_OPTIONS_API__: false,
}

良好的 Tree-Shaking

上面已经通过预定义常量 __DEV__ 的方式来删除生产环境中不必要的代码,但这还不够。

一个大型的工具库或者框架,不一定每个方法都会被使用,对于这些代码,我们希望在打包项目代码的时候能剔除掉,这就需要框架有良好 Tree-Shaking 支持。

我们知道 Tree-Shaking 的工作机制依赖于 ES Module 的静态结构特性,但通常情况下,我们无法保证代码是纯粹的 ESM 模块,此时这部分代码即使没使用,也无法被剔除。

比如我们有如下代码:

// utils.js
function isCanvas(canvas) {
    return canvas.tagName.toLowerCase() !== 'canvas'
}

function getPageWidth() {
    return document.documentElement.clientWidth
}

function getPageHeight() {
    return document.documentElement.clientHeight
}

function getTitle() {
    console.log('get title')
    return document.title
}
const title = getTitle()

export {
    isCanvas,
    getPageWidth,
    getPageHeight,
    title
}

// index.js
import { isCanvas } from './utils'

class Poster {
    constructor() {}
    muntCanvas(canvas, selector) {
        const el = document.querySelector(selector)
        if (__DEV__ && !el) {
            warn(`${el} 元素不存在 ${__DEV__}`)
        }
        if (__DEV__ && isCanvas()) {
            warn(`${el} `)
        }
        (el || document.body).appendChild(canvas)
    }
}

export default Poster

执行打包,生成代码:

image.png

我们发现虽然代码并没有用到 title 属性,但因为 getTitle 存在副作用,所以在打包时,utils.js 里的 getTitle 方法并没有被剔除。

要解决这个问题,我们可以在调用函数时添加魔法注释 /*#__PURE__*/ 来进行标记,告诉打包工具这段代码是可以被删除的:

// utils.js
// ...
function getTitle() {
    console.log('get title')
    return document.title
}
const title = /*#__PURE__*/ getTitle()
// ...

此时打包后的代码,就没有 getTitle 相关逻辑了:

image.png

总结

不论是特性开关还是 Tree-Shaking,本质上都是在控制打包后的文件大小。

善用常量,结合打包工具,删除条件为 false 的语句块。

熟练使用 ES Module,掌握 Tree-Shaking 原理,必要的地方使用 /*#__PURE__*/ 魔法注释以及 sideEffects: false