使用Vue-cli、typeScript开发Vue通用插件

4,068 阅读2分钟

前言

作为渐进式框架,Vue可以灵活地引入各种为它量身定做的插件。想必每一位Vue开发者都想过自己去实现一款Vue插件,将自己的新奇脑洞可以发布到npm社区供其他Vue开发者细品。这里我就自己前一阵使用Vue-cli脚手架和typescript开发Vue插件的环境搭建和注意事项做一个踩坑总结,希望可以为其他想要开发插件的同学提供一点点帮助。

明确目的

在做一个Vue插件之前我们要考虑的是插件的功能是什么、使用者要如何去使用插件以及这些功能的实现方式。

比如vuedraggable库的功能是为使用者提供DOM拖拽功能,可以使用鼠标点击拖拽的方式去变更不同列表中的元素。他的使用方式是template模板组件和js相结合。我们可以通过js来控制组件的状态和数据。

又或者一个alert插件会将呼出提示框的函数绑定到Vue实例上,使用者可以在任何一个组件中通过this.$alert({ options })来呼出提示框。

如何在插件中注册组件或者在插件中为Vue实例提供新的属性将会在下文给予说明。

在明确了我们设计的插件的功能,以及其他开发者要以什么方式去使用之后,就可以着手去开发插件了。

环境搭建

安装脚手架

首先我们需要在全局安装Vue-cli3脚手架。

npm install -g @vue/cli
# OR
yarn global add @vue/cli

新建项目

然后新建我们的项目

vue create hello-plugin

由于我们要使typeScript来规范我们的代码,所以在配置项中选择 Manually selecty features 来自定义我们项目的配置

从截图中可以看到,我们在features中选择了Babel、TypeScript、eslint和Unit test。选择Unit Test是因为我们的插件需要单元测试来保证稳定性,毕竟是要提供给其他开发者使用的,如果不去编写测试用例的话,很容易当其他人使用的时候出现各种意料之外的Bug。

package.json 配置

接下来修改我们的package.json。

{
  "name": "hello-plugin",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "test:unit": "vue-cli-service test:unit",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.4",
    "vue": "^2.6.11",
    "vue-class-component": "^7.2.2",
    "vue-property-decorator": "^8.3.0"
  },
  "devDependencies": {
    "@types/chai": "^4.2.8",
    "@types/mocha": "^5.2.4",
    "@typescript-eslint/eslint-plugin": "^2.18.0",
    "@typescript-eslint/parser": "^2.18.0",
    "@vue/cli-plugin-babel": "^4.2.0",
    "@vue/cli-plugin-eslint": "^4.2.0",
    "@vue/cli-plugin-typescript": "^4.2.0",
    "@vue/cli-plugin-unit-mocha": "^4.2.0",
    "@vue/cli-service": "^4.2.0",
    "@vue/eslint-config-typescript": "^5.0.1",
    "@vue/test-utils": "1.0.0-beta.31",
    "chai": "^4.1.2",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.1.2",
    "typescript": "~3.7.5",
    "vue-template-compiler": "^2.6.11"
  }
}

这是项目创建之后的默认package.json,由于我们开发的插件会被其他的开发者引用,所以如果我们打包的产物中包含Vue包的话可能会引发各种问题。最普遍的问题是用户可能会在引入我们的包之后会在runtime时创建两个不用的Vue实例。所以我们Vue插件的package.json中一定不能将Vue放在dependencies中,而是要放在peerDependencies中,表明会从这个包的引用者的其他的包中引入,而不会在这个包里直接引入。

也就是如下的区别

// dependencies
UserProject
|- node_modules
   |- hello-plugins
      |- node_modules
         |- vue
         

// peerDependencies
UserProject
|- node_modules
   |- hello-plugins
   |- vue

但是我们不能保证使用者也使用了Typescript来开发项目,所以我们要把下面两个ts相关的包留在dependencies中。

"vue-class-component": "^7.2.2",
"vue-property-decorator": "^8.3.0"

此外要表明我们最终产物的位置和打包时候的需要运行的指令。

  "main": "dist/helloPlugin.common.js",
  "types": "dist/lib/src/main.d.ts",
  "scripts": {
    "build": "vue-cli-service build --target lib --name helloPlugin src/main.ts",
  },

build命令表示我们执行npm run build之后实际执行的指令,--target lib --name helloPlugin src/main.ts这一串参数的意思是我们打包出来的是一个库文件,产物的名字为helloPlugin,打包入口是src/main.ts。 详情参见Vue-cli的官方文档

main属性表示如果使用者引入我们这个包将会调用哪个文件作为入口,我们执行build指令之后生成的dist/helloPlugin.common.js就是这个包的入口。

types属性表示如果使用者也使用了typeScript时,这个包提供的类型声明文件入口,这个文件如何生成会在下文提到。

最终修改好的package.json如下:

{
  "name": "hello-plugin",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "test:unit": "vue-cli-service test:unit",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "vue-class-component": "^7.2.2",
    "vue-property-decorator": "^8.3.0"
  },
  "devDependencies": {
    "@types/chai": "^4.2.8",
    "@types/mocha": "^5.2.4",
    "@typescript-eslint/eslint-plugin": "^2.18.0",
    "@typescript-eslint/parser": "^2.18.0",
    "@vue/cli-plugin-babel": "^4.2.0",
    "@vue/cli-plugin-eslint": "^4.2.0",
    "@vue/cli-plugin-typescript": "^4.2.0",
    "@vue/cli-plugin-unit-mocha": "^4.2.0",
    "@vue/cli-service": "^4.2.0",
    "@vue/eslint-config-typescript": "^5.0.1",
    "@vue/test-utils": "1.0.0-beta.31",
    "chai": "^4.1.2",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.1.2",
    "typescript": "~3.7.5",
    "vue-template-compiler": "^2.6.11",
    "core-js": "^3.6.4",
    "vue": "^2.6.11"
  },
  "peerDependencies": {
    "core-js": "^3.6.4",
    "vue": "^2.6.11"
  }
}

ts相关处理

生成声明文件

首先如何生成.d.ts文件呢,我们在tsconfig.ts中加入如下的配置:

compierOptions: {
    "declaration": true,
    "declarationDir": "dist/lib",
}

这样在编译的时候,我们的所有以ts为后缀的文件都会生成相应的.d.ts文件,并被保存在dist/lib目录下。

但是神奇的是你会发现并没有生成你期望中的.d.ts文件

如果你是一个标准的ts项目,这样配置是没问题的,但是Vue-cli在webpack打包和编译的时候做了并行化处理,我们必须关闭并行打包的一些相关配置才能生成心心念念的类型声明文件。所以我们要修改vue-cli的配置。

// vue.config.js
module.exports = {
  chainWebpack: (config) => {
    // These are some necessary steps changing the default webpack config of the Vue CLI
    // that need to be changed in order for Typescript based components to generate their
    // declaration (.d.ts) files.
    //
    // Discussed here https://github.com/vuejs/vue-cli/issues/1081
    if (process.env.NODE_ENV === 'production') {
      config.module.rule('ts').uses.delete('cache-loader');

      config.module
        .rule('ts')
        .use('ts-loader')
        .loader('ts-loader')
        .tap((opts) => {
          opts.transpileOnly = false;
          opts.happyPackMode = false;
          return opts;
        });
    }
  },
  parallel: false,
};

模块补充

如果我们需要为Vue对象添加新的属性,使用者在使用的时候ts一定会提示Vue对象没有这个属性。所以我们需要把下面的声明放在main.ts中,为Vue的类型声明中添加我们自己的属性

// main.ts
declare module 'vue/types/vue' {
  interface Vue {
    $alert: IAlert;
  }
}

插件开发

最后就是愉快的coding环节了。Vue插件开发的最佳指引当然是 官方文档

使用者调用Vue.use(helloPlugin)来注册我们的插件,当然也可以在注册的时候提供一些初始化参数Vue.use(helloPlugin, { ...options })

Vue.use会调用我们插件暴露的install函数,并将Vue -- Vue对象 和 options -- 用户提供的参数 作为参数传递到我们的install函数中。

有了Vue对象我们就可以随心所欲去在他的基础上进行扩展了。以下是官方文档中提供的四个使用方式。

MyPlugin.install = function (Vue, options) {
  // 1. add global method or property
  Vue.myGlobalMethod = function () {
    // some logic ...
  }

  // 2. add a global asset
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      // some logic ...
    }
    ...
  })

  // 3. inject some component options
  Vue.mixin({
    created: function () {
      // some logic ...
    }
    ...
  })

  // 4. add an instance method
  Vue.prototype.$myMethod = function (methodOptions) {
    // some logic ...
  }
}

分别是:

  • 为Vue对象注册全局方法或属性
  • 添加例如v-on的全局指令
  • 为组件提供mixins,这些mixins会混入所有用户组件中。
  • 为Vue实例添加方法或属性

当然我们可以使用Vue 的全部功能,比如为使用者提供我们的组件:

Vue.component('HelloComponent', HelloComponent);

在插件中注册的组件,用户可以在任意位置的模板中使用,是不是很方便呢。

你也可以去思考其他的开发模式,为插件的使用者提供各种快乐的开发体验。

Over

各位,加油。