├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README_zh-CN.md ├── build ├── build.js └── configs.js ├── dist ├── vue-create-api.esm.js ├── vue-create-api.js └── vue-create-api.min.js ├── examples ├── dialog │ ├── app.js │ ├── components │ │ ├── App.vue │ │ ├── child.vue │ │ └── dialog.vue │ └── index.html ├── global.css ├── index.html ├── server.js ├── webpack.config.js └── webpack.test.js ├── package-lock.json ├── package.json ├── src ├── cache.js ├── creator.js ├── debug.js ├── index.js ├── instantiate.js ├── parse.js └── util.js ├── test └── unit │ ├── .eslintrc │ ├── components │ ├── app.vue │ └── dialog.vue │ ├── index.js │ └── karma.conf.js └── types ├── index.d.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ], 10 | "plugins": [ 11 | "transform-object-rest-spread", 12 | "transform-object-assign" 13 | ], 14 | "env": { 15 | "test": { 16 | "plugins": [ "istanbul" ] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | example/modules/*.js 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 8 | extends: 'standard', 9 | // required to lint *.vue files 10 | plugins: [ 11 | 'html' 12 | ], 13 | // add your custom rules here 14 | 'rules': { 15 | // allow debugger during development 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 17 | 'space-before-function-paren': 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | deploy/ 7 | test/unit/coverage 8 | test/e2e/reports 9 | selenium-debug.log 10 | 11 | # Test publish files 12 | package/ 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # 指定语言 https://docs.travis-ci.com/user/languages/javascript-with-nodejs/ 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | node_js: 7 | - "6" 8 | branches: 9 | only: 10 | - master 11 | script: 12 | - npm test 13 | notifications: 14 | webhooks: 15 | urls: 16 | - https://www.travisbuddy.com/ 17 | on_success: never 18 | on_failure: always 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 cube-ui 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-create-api 2 | A Vue plugin which make Vue component invocated by API. 3 | 4 | [中文文档](https://github.com/cube-ui/vue-create-api/blob/master/README_zh-CN.md) 5 | 6 | ## Installing 7 | 8 | use npm 9 | 10 | ``` 11 | $ npm install vue-create-api 12 | ``` 13 | 14 | use cdn 15 | 16 | ``` 17 | 18 | ``` 19 | 20 | ## Usage 21 | 22 | ``` js 23 | import CreateAPI from 'vue-create-api' 24 | 25 | Vue.use(CreateAPI) 26 | 27 | // or with options. 28 | 29 | Vue.use(CreateAPI, { 30 | componentPrefix: 'cube-' 31 | apiPrefix: '$create-' 32 | }) 33 | 34 | // then the Vue constructor will have the createAPI function. 35 | 36 | import Dialog from './components/dialog.vue' 37 | 38 | // make Dialog component invocated by API. 39 | 40 | Vue.createAPI(Dialog, true) 41 | 42 | // use in general JS files. 43 | // however, the $props can not be reactive. 44 | 45 | Dialog.$create({ 46 | $props: { 47 | title: 'Hello', 48 | content: 'I am from pure JS' 49 | } 50 | }).show() 51 | 52 | // use in a vue component. 53 | 54 | this.$createDialog({ 55 | $props: { 56 | title: 'Hello', 57 | content: 'I am from a vue component' 58 | }, 59 | }).show() 60 | ``` 61 | 62 | ```ts 63 | // typescript 64 | import CreateAPI from 'vue-create-api' 65 | 66 | Vue.use(CreateAPI) 67 | 68 | Vue.createAPI(Dialog, events, single) 69 | 70 | this.$createDialog({ 71 | $props: { 72 | title: 'Hello', 73 | content: 'I am from a vue component' 74 | } 75 | }).show() 76 | ``` 77 | ```ts 78 | // d.ts 79 | import Vue, { VueConstructor } from 'vue' 80 | import { createFunction } from 'vue-create-api'; 81 | 82 | export declare class UIComponent extends Vue { 83 | show ():void 84 | hide ():void 85 | } 86 | 87 | declare module 'vue/types/vue' { 88 | interface Vue { 89 | /** create Dialog instance */ 90 | $createDialog: createFunction 91 | } 92 | } 93 | ``` 94 | ### Tip 95 | 96 | > using typescript, `terser-webpack-plugin`(vue-cli3.x) or `uglifyjs`(vue-cli2.x) adds `{ keep_fnames: true }` 97 | 98 | ## Constructor Options 99 | 100 | |key|description|default| 101 | |:---|---|---| 102 | | `componentPrefix`|the prefix name of your component| - | 103 | |`apiPrefix`|the api prefix|`$create`| 104 | 105 | ## Methods 106 | 107 | ### Vue.createAPI(Component, [single]) 108 | 109 | - Parameters: 110 | 111 | - `{Function | Object} Component` Vue component which must contains `name` 112 | - `{Boolean} [single]` whether singleton 113 | 114 | - Usage: 115 | 116 | - This method will add a method which is named `$create{camelize(Component.name)}` to Vue's prototype, so you can instantiate the Vue component by `const instance = this.$createAaBb(config, [renderFn, single])` in other components. The instantiated component's template content will be attached to `body` element. 117 | 118 | - `const instance = this.$createAaBb(config, renderFn, single)` 119 | 120 | **Parameters:** 121 | 122 | | Attribute | Description | Type | Accepted Values | Default | 123 | | - | - | - | - | - | 124 | | config | Config options | Object | {} | - | 125 | | renderFn | Optional, used to generate the VNode child node in the slot scene in general | Function | - | function (createElement) {...} | 126 | | single | Optional, whether the instantiated component is a singleton or not. If two parameters are provided and the `renderFn`'s type is not function, then the `single` value is the sencond parameter's value. | Boolean | true/false | single in createAPI() | 127 | 128 | **Config options `config`:** 129 | 130 | You can set `$props` and `$events` in `config`, `$props` supported reactive properties, these props will be watched. 131 | 132 | | Attribute | Description | Type | Accepted Values | Default | 133 | | - | - | - | - | - | 134 | | $props | Component props | Object | - | {
title: 'title',
content: 'my content',
open: false
} | 135 | | $events | Component event handlers | Object | - | {
click: 'clickHandler',
select: this.selectHandler
} | 136 | 137 | `$props` example, `{ [key]: [propKey] }`: 138 | 139 | ```js 140 | { 141 | title: 'title', 142 | content: 'my content', 143 | open: false 144 | } 145 | ``` 146 | 147 | `title`, `content` and `open` are keys of the component prop or data, and the prop' value will be taken by the following steps: 148 | 149 | 1. If `propKey` is not a string value, then use `propKey` as the prop value. 150 | 1. If `propKey` is a string value and the caller instance dont have the `propKey` property, then use `propKey` as the prop value. 151 | 1. If `propKey` is a string value and the caller instance have the `propKey` property, then use the caller's `propKey` property value as the prop value. And the prop value will be reactive. 152 | 153 | `$events` example, `{ [eventName]: [eventValue] }`: 154 | 155 | ```js 156 | { 157 | click: 'clickHandler', 158 | select: this.selectHandler 159 | } 160 | ``` 161 | 162 | `click` and `select` are event names, and the event handlers will be taken by the following steps: 163 | 164 | 1. If `eventValue` is not a string value, then use `eventValue` as the event handler. 165 | 1. If `eventValue` is a string value, then use the caller's `eventValue` property value as the event handler. 166 | 167 | You can set [all avaliable properties in Vue](https://vuejs.org/v2/guide/render-function.html#The-Data-Object-In-Depth), but you need to add prefix `$`, eg: 168 | 169 | ```js 170 | this.$createAaBb({ 171 | $attrs: { 172 | id: 'id' 173 | }, 174 | $class: { 175 | 'my-class': true 176 | } 177 | }) 178 | ``` 179 | 180 | **The Returned value `instance`:** 181 | 182 | `instance` is a instantiated Vue component. 183 | > And the `remove` method will be **attached** to this instance. 184 | 185 | You can invoke the `remove` method to destroy the component and detach the component's content from `body` element. 186 | 187 | If the caller is destroyed and the `instance` will be automatically destroyed. 188 | 189 | - Example: 190 | 191 | First we create Hello.vue component: 192 | 193 | ```html 194 | 200 | 201 | 217 | ``` 218 | 219 | Then we make Hello.vue as an API style component by calling the `createAPI` method. 220 | 221 | ```js 222 | import Vue from 'vue' 223 | import Hello from './Hello.vue' 224 | import CreateAPI from 'vue-create-api' 225 | Vue.use(CreateAPI) 226 | 227 | // create this.$createHello API 228 | Vue.createAPI(Hello, true) 229 | 230 | // init Vue 231 | new Vue({ 232 | el: '#app', 233 | render: function (h) { 234 | return h('button', { 235 | on: { 236 | click: this.showHello 237 | } 238 | }, ['Show Hello']) 239 | }, 240 | methods: { 241 | showHello() { 242 | const instance = this.$createHello({ 243 | $props: { 244 | content: 'My Hello Content', 245 | }, 246 | $events: { 247 | click() { 248 | console.log('Hello component clicked.') 249 | instance.remove() 250 | } 251 | } 252 | }, /* renderFn */ (createElement) => { 253 | return [ 254 | createElement('p', { 255 | slot: 'other' 256 | }, 'other content') 257 | ] 258 | }) 259 | } 260 | } 261 | }) 262 | ``` 263 | In this example, we create a component `Hello` which needs to be invoked in api form and we invoke it in another component.The focus is what `showHello()` does: invoking method `this.$createHello(config, renderFn)` to instantiate `Hello`. 264 | 265 | ### How to use in general JS files or use it in global 266 | 267 | In vue component, you can call by `this.$createHello(config, renderFn)` because the `this` is just a Vue instance. But in general JS files, you need to use `Hello.$create`. As shown below: 268 | 269 | ```js 270 | import Vue from 'vue' 271 | import Hello from './Hello.vue' 272 | import CreateAPI from 'vue-create-api' 273 | Vue.use(CreateAPI) 274 | 275 | // create this.$createHello and Hello.create API 276 | Vue.createAPI(Hello, true) 277 | 278 | Hello.$create(config, renderFn) 279 | ``` 280 | 281 | Notice, when we use in general JS files, we can't make props be reactive. 282 | 283 | ### batchDestroy 284 | 285 | We can use the `batchDestroy` method provided by `vue-create-api` to destroy all instances uniformly. For example, we can destroy them uniformly when the route is switched: 286 | 287 | ```js 288 | import Vue from 'vue' 289 | import VueRouter from 'vue-router' 290 | import CreateAPI from 'vue-create-api' 291 | 292 | Vue.use(VueRouter) 293 | 294 | const router = new VueRouter({ routes: [] }) 295 | router.afterEach(() => { 296 | CreateAPI.batchDestroy() 297 | }) 298 | ``` 299 | 300 | `batchDestroy` can receive a filter function to determine which instances need to be destroyed: 301 | 302 | ```js 303 | CreateAPI.batchDestroy(instances => instances.filter(ins => ins)) 304 | ``` -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 | # vue-create-api 2 | 3 | 一个能够让 Vue 组件通过 API 方式调用的插件。 4 | 5 | ## 安装 6 | 7 | 通过 npm 8 | 9 | ``` 10 | $ npm install vue-create-api 11 | ``` 12 | 13 | 通过 cdn 14 | 15 | ``` 16 | 17 | ``` 18 | 19 | ## 使用 20 | 21 | ``` js 22 | import CreateAPI from 'vue-create-api' 23 | 24 | Vue.use(CreateAPI) 25 | 26 | // 也可以传递一个配置项 27 | 28 | Vue.use(CreateAPI, { 29 | componentPrefix: 'cube-' 30 | apiPrefix: '$create-' 31 | }) 32 | 33 | // 之后会在 Vue 构造器下添加一个 createAPI 方法 34 | 35 | import Dialog from './components/dialog.vue' 36 | 37 | // 调用 createAPI 生成对应 API,并挂载到 Vue.prototype 和 Dialog 对象上 38 | 39 | Vue.createAPI(Dialog, true) 40 | 41 | // 之后便可以在普通的 js 文件中使用,但是 $props 不具有响应式 42 | 43 | Dialog.$create({ 44 | $props: { 45 | title: 'Hello', 46 | content: 'I am from pure JS' 47 | } 48 | }).show() 49 | 50 | // 在 vue 组件中可以通过 this 调用 51 | 52 | this.$createDialog({ 53 | $props: { 54 | title: 'Hello', 55 | content: 'I am from a vue component' 56 | }, 57 | }).show() 58 | ``` 59 | ```ts 60 | // typescript 使用方式 61 | import CreateAPI from 'vue-create-api' 62 | 63 | Vue.use(CreateAPI) 64 | 65 | Vue.createAPI(Dialog, events, single) 66 | 67 | this.$createDialog({ 68 | $props: { 69 | title: 'Hello', 70 | content: 'I am from a vue component' 71 | } 72 | }).show() 73 | ``` 74 | ```ts 75 | // 自定义 d.ts 扩展通过api创建的方法 76 | import Vue, { VueConstructor } from 'vue' 77 | import { createFunction } from 'vue-create-api'; 78 | 79 | export declare class UIComponent extends Vue { 80 | show ():void 81 | hide ():void 82 | } 83 | 84 | declare module 'vue/types/vue' { 85 | interface Vue { 86 | /** create Dialog instance */ 87 | $createDialog: createFunction 88 | } 89 | } 90 | ``` 91 | ### 提示 92 | 93 | > 使用typescript时, `terser-webpack-plugin`(vue-cli3.x)或`uglifyjs`(vue-cli2.x)增加`{ keep_fnames: true }` 94 | 95 | ## 构造器配置项 96 | 97 | | 键名 | 描述 | 默认值 | 98 | | :--- | --- | --- | 99 | | `componentPrefix` | 组件名前缀,最终生成的 API 会忽略该前缀 | - | 100 | | `apiPrefix` | 为生成的 API 添加统一前缀 | `$create` | 101 | 102 | ## 方法 103 | 104 | ### Vue.createAPI(Component, [single]) 105 | 106 | - 参数: 107 | 108 | - `{Function | Object} Component` Vue 组件必须要有组件名 `name` 109 | - `{Boolean} [single]` 是否采用单例模式实例化组件 110 | 111 | - 使用: 112 | 113 | - 调用该方法会在 Vue.prototype 上添加名为 `$create{camelize(Component.name)}` 的方法, 之后在其他 vue 组件中可以通过 `const instance = this.$createAaBb(config, [renderFn, single])` 实例化该组件。组件实例化后对应的 DOM 元素会添加到 `body` 中。 114 | 115 | - `const instance = this.$createAaBb(config, renderFn, single)` 116 | 117 | **参数:** 118 | 119 | | 名称 | 描述 | 类型 | 可选值 | 默认值 | 120 | | - | - | - | - | - | 121 | | config | 配置参数 | Object | {} | - | 122 | | renderFn | 可选参数,用于生成子 VNode 节点,通常用于处理插槽 | Function | - | function (createElement) {...} | 123 | | single | 可选参数, 决定实例化是否采用单例模式。在没有传递 renderFn 时,可以直接作为第二个参数传入。 | Boolean | true/false | 调用 createAPI 时传入的 single 值 | 124 | 125 | **配置项 `config`:** 126 | 127 | 你可以在 `config` 中配置 `$props` 和 `$events`, `$props` 中的属性会被 watch,从而支持响应式更新. 128 | 129 | | 属性 | 描述 | 类型 | 可选值 | 默认值 | 130 | | - | - | - | - | - | 131 | | $props | 组件的 Prop | Object | - | {
title: 'title',
content: 'my content',
open: false
} | 132 | | $events | 组件的事件回调 | Object | - | {
click: 'clickHandler',
select: this.selectHandler
} | 133 | 134 | `$props` 示例, `{ [key]: [propKey] }`: 135 | 136 | ```js 137 | { 138 | title: 'title', 139 | content: 'my content', 140 | open: false 141 | } 142 | ``` 143 | 144 | `title`, `content` 和 `open` 是传递给组件的 Prop 键名, 而 Prop 对应的值采用下面的步骤得到: 145 | 146 | 1. 如果 `propKey` 不是一个字符串, 则直接取 `propKey` 作为该 Prop 值。 147 | 1. 如果 `propKey` 是一个字符串,但该字符串并没有作为属性名存在于调用 `$createAaBb()` 的组件中,则直接取 `propKey` 这个字符串作为该 Prop 值。 148 | 1. 如果 `propKey` 是一个字符串,且作为属性名存在于调用 `$createAaBb()` 的组件中, 则会取该实例对应的属性值作为该 Prop 值。 同时会 watch 该属性,做到响应式更新。 149 | 150 | `$events` 示例, `{ [eventName]: [eventValue] }`: 151 | 152 | ```js 153 | { 154 | click: 'clickHandler', 155 | select: this.selectHandler 156 | } 157 | ``` 158 | 159 | `click` 和 `select` 是事件名, 同时对应的事件回调会通过下面的步骤确定: 160 | 161 | 1. 如果 `eventValue` 不是一个字符串, 那么直接取 `eventValue` 作为事件回调. 162 | 1. 如果 `eventValue` 是一个字符串, 那么会取调用 `$createAaBb` 的组件中以 `eventValue` 作为属性名的值,当做事件回调. 163 | 164 | 同时,config 中可以设置 Vue 支持的[所有的配置值](https://cn.vuejs.org/v2/guide/render-function.html#%E6%B7%B1%E5%85%A5-data-%E5%AF%B9%E8%B1%A1),但是必须要加 `$`。比如: 165 | 166 | ```js 167 | this.$createAaBb({ 168 | $attrs: { 169 | id: 'id' 170 | }, 171 | $class: { 172 | 'my-class': true 173 | } 174 | }) 175 | ``` 176 | 177 | **返回值 `instance`:** 178 | 179 | `instance` 是一个实例化的 Vue 组件。 180 | > 实例上会包含一个 `remove` 方法。你可以调用 `remove` 方法去销毁该组件,同时原本添加到 `body` 下的 DOM 元素也会删除。 181 | 182 | 如果调用 `$createAaBb` 的组件销毁了,那么该组件也会自动销毁。 183 | 184 | - 示例: 185 | 186 | 首先我们先创建一个 Hello.vue 组件: 187 | 188 | ```html 189 | 195 | 196 | 212 | ``` 213 | 214 | 然后我们通过调用 `createAPI`,得到一个可以命令式创建该组件的 API 。 215 | 216 | ```js 217 | import Vue from 'vue' 218 | import Hello from './Hello.vue' 219 | import CreateAPI from 'vue-create-api' 220 | Vue.use(CreateAPI) 221 | 222 | // 得到 this.$createHello API,它会添加到 Vue 原型上 223 | Vue.createAPI(Hello, true) 224 | 225 | // 实例化 Vue 226 | new Vue({ 227 | el: '#app', 228 | render: function (h) { 229 | return h('button', { 230 | on: { 231 | click: this.showHello 232 | } 233 | }, ['Show Hello']) 234 | }, 235 | methods: { 236 | showHello() { 237 | const instance = this.$createHello({ 238 | $props: { 239 | content: 'My Hello Content', 240 | }, 241 | $events: { 242 | click() { 243 | console.log('Hello component clicked.') 244 | instance.remove() 245 | } 246 | } 247 | }, /* renderFn */ (createElement) => { 248 | return [ 249 | createElement('p', { 250 | slot: 'other' 251 | }, 'other content') 252 | ] 253 | }) 254 | } 255 | } 256 | }) 257 | ``` 258 | 259 | 在该示例中,我们创建了一个 `Hello` 组件,利用 `createAPI()` 你可以在其他组件中调用 API 去创建该组件。可以看到,在 `showHello()` 方法中,通过 `this.$createHello(config, renderFn)` 可以实例化 `Hello` 组件。 260 | 261 | ### 如何在普通 js 文件中或者全局调用 262 | 263 | 由于使用 `createAPI()` 生成实例化组件的 API 时,会将该 API 添加到 Vue 原型上,因此在 Vue 实例中,可以直接通过 `this.$createHello(config, renderFn)` 创建组件。而如果在普通 JS 中,可以通过组件自身的 `$create` 来进行实例化了,因为我们同样将该 API 添加到了组件自身上,比如: 264 | 265 | ```js 266 | import Vue from 'vue' 267 | import Hello from './Hello.vue' 268 | import CreateAPI from 'vue-create-api' 269 | Vue.use(CreateAPI) 270 | 271 | // 得到 Vue.prototype.$createHello 和 Hello.create API 272 | Vue.createAPI(Hello, true) 273 | 274 | Hello.$create(config, renderFn) 275 | ``` 276 | 277 | 注意:当我们在普通 JS 文件中使用时,无法让 Prop 响应式更新。 278 | 279 | ### 批量销毁 280 | 281 | 我们可以通过`vue-create-api`提供的`batchDestroy`方法统一销毁所有实例,例如我们可以在路由切换的时候进行统一销毁: 282 | 283 | ```js 284 | import Vue from 'vue' 285 | import VueRouter from 'vue-router' 286 | import CreateAPI from 'vue-create-api' 287 | 288 | Vue.use(VueRouter) 289 | 290 | const router = new VueRouter({ routes: [] }) 291 | router.afterEach(() => { 292 | CreateAPI.batchDestroy() 293 | }) 294 | ``` 295 | 296 | `batchDestroy`可以接收一个过滤函数,决定哪些实例需要销毁: 297 | 298 | ```js 299 | CreateAPI.batchDestroy(instances => instances.filter(ins => ins)) 300 | ``` -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const zlib = require('zlib') 4 | const uglify = require('uglify-js') 5 | const rollup = require('rollup') 6 | const configs = require('./configs') 7 | 8 | if (!fs.existsSync('dist')) { 9 | fs.mkdirSync('dist') 10 | } 11 | 12 | build(Object.keys(configs).map(key => configs[key])) 13 | 14 | function build (builds) { 15 | let built = 0 16 | const total = builds.length 17 | const next = () => { 18 | buildEntry(builds[built]).then(() => { 19 | built++ 20 | if (built < total) { 21 | next() 22 | } 23 | }).catch(logError) 24 | } 25 | 26 | next() 27 | } 28 | 29 | function buildEntry ({ input, output }) { 30 | const isProd = /min\.js$/.test(output.file) 31 | return rollup.rollup(input) 32 | .then(bundle => bundle.generate(output)) 33 | .then(({ code }) => { 34 | if (isProd) { 35 | var minified = (output.banner ? output.banner + '\n' : '') + uglify.minify(code, { 36 | output: { 37 | /* eslint-disable camelcase */ 38 | ascii_only: true 39 | /* eslint-enable camelcase */ 40 | } 41 | }).code 42 | return write(output.file, minified, true) 43 | } else { 44 | return write(output.file, code) 45 | } 46 | }) 47 | } 48 | 49 | function write (dest, code, zip) { 50 | return new Promise((resolve, reject) => { 51 | function report (extra) { 52 | console.log(blue(path.relative(process.cwd(), dest)) + ' ' + getSize(code) + (extra || '')) 53 | resolve() 54 | } 55 | 56 | fs.writeFile(dest, code, err => { 57 | if (err) return reject(err) 58 | if (zip) { 59 | zlib.gzip(code, (err, zipped) => { 60 | if (err) return reject(err) 61 | report(' (gzipped: ' + getSize(zipped) + ')') 62 | }) 63 | } else { 64 | report() 65 | } 66 | }) 67 | }) 68 | } 69 | 70 | function getSize (code) { 71 | return (code.length / 1024).toFixed(2) + 'kb' 72 | } 73 | 74 | function logError (e) { 75 | console.log(e) 76 | } 77 | 78 | function blue (str) { 79 | return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m' 80 | } 81 | -------------------------------------------------------------------------------- /build/configs.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const replace = require('rollup-plugin-replace') 3 | const babel = require('rollup-plugin-babel') 4 | const version = process.env.VERSION || require('../package.json').version 5 | const banner = 6 | `/** 7 | * vue-create-api v${version} 8 | * (c) ${new Date().getFullYear()} ustbhuangyi 9 | * @license MIT 10 | */` 11 | 12 | const resolve = _path => path.resolve(__dirname, '../', _path) 13 | 14 | const configs = { 15 | umdDev: { 16 | input: resolve('src/index.js'), 17 | file: resolve('dist/vue-create-api.js'), 18 | format: 'umd', 19 | env: 'development' 20 | }, 21 | umdProd: { 22 | input: resolve('src/index.js'), 23 | file: resolve('dist/vue-create-api.min.js'), 24 | format: 'umd', 25 | env: 'production' 26 | }, 27 | esm: { 28 | input: resolve('src/index.js'), 29 | file: resolve('dist/vue-create-api.esm.js'), 30 | format: 'es' 31 | } 32 | } 33 | 34 | function genConfig(opts) { 35 | const config = { 36 | input: { 37 | input: opts.input, 38 | plugins: [ 39 | replace({ 40 | __VERSION__: version 41 | }), 42 | babel({ 43 | exclude: 'node_modules/**', 44 | plugins: ['external-helpers'] 45 | }) 46 | ] 47 | }, 48 | output: { 49 | banner, 50 | file: opts.file, 51 | format: opts.format, 52 | name: 'VueCreateAPI' 53 | } 54 | } 55 | 56 | if (opts.env) { 57 | config.input.plugins.unshift(replace({ 58 | 'process.env.NODE_ENV': JSON.stringify(opts.env) 59 | })) 60 | } 61 | 62 | return config 63 | } 64 | 65 | function mapValues(obj, fn) { 66 | const res = {} 67 | Object.keys(obj).forEach(key => { 68 | res[key] = fn(obj[key], key) 69 | }) 70 | return res 71 | } 72 | 73 | module.exports = mapValues(configs, genConfig) 74 | -------------------------------------------------------------------------------- /dist/vue-create-api.esm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * vue-create-api v0.2.3 3 | * (c) 2025 ustbhuangyi 4 | * @license MIT 5 | */ 6 | var _extends = Object.assign || function (target) { 7 | for (var i = 1; i < arguments.length; i++) { 8 | var source = arguments[i]; 9 | 10 | for (var key in source) { 11 | if (Object.prototype.hasOwnProperty.call(source, key)) { 12 | target[key] = source[key]; 13 | } 14 | } 15 | } 16 | 17 | return target; 18 | }; 19 | 20 | var camelizeRE = /-(\w)/g; 21 | 22 | function camelize(str) { 23 | return (str + '').replace(camelizeRE, function (m, c) { 24 | return c ? c.toUpperCase() : ''; 25 | }); 26 | } 27 | 28 | function escapeReg(str, delimiter) { 29 | return (str + '').replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + (delimiter || '') + '-]', 'g'), '\\$&'); 30 | } 31 | 32 | function isBoolean(value) { 33 | return typeof value === 'boolean'; 34 | } 35 | 36 | function isUndef(value) { 37 | return value === undefined; 38 | } 39 | 40 | function isStr(value) { 41 | return typeof value === 'string'; 42 | } 43 | 44 | function isFunction(fn) { 45 | return typeof fn === 'function'; 46 | } 47 | 48 | function isArray(arr) { 49 | return Object.prototype.toString.call(arr) === '[object Array]'; 50 | } 51 | 52 | function assert(condition, msg) { 53 | if (!condition) { 54 | throw new Error("[vue-create-api error]: " + msg); 55 | } 56 | } 57 | 58 | function instantiateComponent(Vue, Component, data, renderFn, options) { 59 | var renderData = void 0; 60 | var childrenRenderFn = void 0; 61 | 62 | var instance = new Vue(_extends({}, options, { 63 | render: function render(createElement) { 64 | var children = childrenRenderFn && childrenRenderFn(createElement); 65 | if (children && !Array.isArray(children)) { 66 | children = [children]; 67 | } 68 | 69 | return createElement(Component, _extends({}, renderData), children || []); 70 | }, 71 | 72 | methods: { 73 | init: function init() { 74 | document.body.appendChild(this.$el); 75 | }, 76 | destroy: function destroy() { 77 | this.$destroy(); 78 | if (this.$el && this.$el.parentNode === document.body) { 79 | document.body.removeChild(this.$el); 80 | } 81 | } 82 | } 83 | })); 84 | instance.updateRenderData = function (data, render) { 85 | renderData = data; 86 | childrenRenderFn = render; 87 | }; 88 | instance.updateRenderData(data, renderFn); 89 | instance.$mount(); 90 | instance.init(); 91 | var component = instance.$children[0]; 92 | component.$updateProps = function (props) { 93 | _extends(renderData.props, props); 94 | instance.$forceUpdate(); 95 | }; 96 | return component; 97 | } 98 | 99 | function parseRenderData() { 100 | var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 101 | var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 102 | 103 | events = parseEvents(events); 104 | var props = _extends({}, data); 105 | var on = {}; 106 | for (var name in events) { 107 | if (events.hasOwnProperty(name)) { 108 | var handlerName = events[name]; 109 | if (props[handlerName]) { 110 | on[name] = props[handlerName]; 111 | delete props[handlerName]; 112 | } 113 | } 114 | } 115 | return { 116 | props: props, 117 | on: on 118 | }; 119 | } 120 | 121 | function parseEvents(events) { 122 | var parsedEvents = {}; 123 | events.forEach(function (name) { 124 | parsedEvents[name] = camelize('on-' + name); 125 | }); 126 | return parsedEvents; 127 | } 128 | 129 | var instances = []; 130 | 131 | function add(component) { 132 | var ins = void 0; 133 | var len = instances.length; 134 | for (var i = 0; i < len; i += 1) { 135 | ins = instances[i]; 136 | if (ins === component) { 137 | return; 138 | } 139 | } 140 | instances.push(component); 141 | } 142 | 143 | function remove(component) { 144 | var ins = void 0; 145 | var len = instances.length; 146 | for (var i = 0; i < len; i += 1) { 147 | ins = instances[i]; 148 | if (ins === component) { 149 | instances.splice(i, 1); 150 | } 151 | } 152 | } 153 | 154 | function batchDestroy(filter) { 155 | var hasFilter = isFunction(filter); 156 | var instancesCopy = instances.slice(); 157 | var _instances = hasFilter ? filter(instancesCopy) : instancesCopy; 158 | if (!isArray(_instances)) { 159 | return; 160 | } 161 | _instances.forEach(function (ins) { 162 | if (ins && isFunction(ins.remove)) { 163 | ins.remove(); 164 | } 165 | }); 166 | } 167 | 168 | var eventBeforeDestroy = 'hook:beforeDestroy'; 169 | 170 | function apiCreator(Component) { 171 | var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 172 | var single = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; 173 | 174 | var Vue = this; 175 | var singleMap = {}; 176 | var beforeHooks = []; 177 | 178 | function createComponent(renderData, renderFn, options, single, ownerInstance) { 179 | beforeHooks.forEach(function (before) { 180 | before(renderData, renderFn, single); 181 | }); 182 | var ownerInsUid = options.parent ? options.parent._uid : -1; 183 | 184 | var _ref = singleMap[ownerInsUid] ? singleMap[ownerInsUid] : {}, 185 | comp = _ref.comp, 186 | ins = _ref.ins; 187 | 188 | if (single && comp && ins) { 189 | ins.updateRenderData(renderData, renderFn); 190 | ins.$forceUpdate(); 191 | return comp; 192 | } 193 | var component = instantiateComponent(Vue, Component, renderData, renderFn, options); 194 | var instance = component.$parent; 195 | var originRemove = component.remove; 196 | var isInVueInstance = !!ownerInstance.$on; 197 | 198 | component.remove = function () { 199 | if (isInVueInstance) { 200 | cancelWatchProps(ownerInstance); 201 | } 202 | if (single) { 203 | if (!singleMap[ownerInsUid]) { 204 | return; 205 | } 206 | singleMap[ownerInsUid] = null; 207 | } 208 | originRemove && originRemove.apply(this, arguments); 209 | instance.destroy(); 210 | remove(component); 211 | }; 212 | 213 | var originShow = component.show; 214 | component.show = function () { 215 | originShow && originShow.apply(this, arguments); 216 | return this; 217 | }; 218 | 219 | var originHide = component.hide; 220 | component.hide = function () { 221 | originHide && originHide.apply(this, arguments); 222 | return this; 223 | }; 224 | 225 | if (single) { 226 | singleMap[ownerInsUid] = { 227 | comp: component, 228 | ins: instance 229 | }; 230 | } 231 | return component; 232 | } 233 | 234 | function processProps(ownerInstance, renderData, isInVueInstance, onChange) { 235 | var $props = renderData.props.$props; 236 | if ($props) { 237 | delete renderData.props.$props; 238 | 239 | var watchKeys = []; 240 | var watchPropKeys = []; 241 | Object.keys($props).forEach(function (key) { 242 | var propKey = $props[key]; 243 | if (isStr(propKey) && propKey in ownerInstance) { 244 | // get instance value 245 | renderData.props[key] = ownerInstance[propKey]; 246 | watchKeys.push(key); 247 | watchPropKeys.push(propKey); 248 | } else { 249 | renderData.props[key] = propKey; 250 | } 251 | }); 252 | if (isInVueInstance) { 253 | var unwatchFn = ownerInstance.$watch(function () { 254 | var props = {}; 255 | watchKeys.forEach(function (key, i) { 256 | props[key] = ownerInstance[watchPropKeys[i]]; 257 | }); 258 | return props; 259 | }, onChange); 260 | ownerInstance.__unwatchFns__.push(unwatchFn); 261 | } 262 | } 263 | } 264 | 265 | function processEvents(renderData, ownerInstance) { 266 | var $events = renderData.props.$events; 267 | if ($events) { 268 | delete renderData.props.$events; 269 | 270 | Object.keys($events).forEach(function (event) { 271 | var eventHandler = $events[event]; 272 | if (typeof eventHandler === 'string') { 273 | eventHandler = ownerInstance[eventHandler]; 274 | } 275 | renderData.on[event] = eventHandler; 276 | }); 277 | } 278 | } 279 | 280 | function process$(renderData) { 281 | var props = renderData.props; 282 | Object.keys(props).forEach(function (prop) { 283 | if (prop.charAt(0) === '$') { 284 | renderData[prop.slice(1)] = props[prop]; 285 | delete props[prop]; 286 | } 287 | }); 288 | } 289 | 290 | function cancelWatchProps(ownerInstance) { 291 | if (ownerInstance.__unwatchFns__) { 292 | ownerInstance.__unwatchFns__.forEach(function (unwatchFn) { 293 | unwatchFn(); 294 | }); 295 | ownerInstance.__unwatchFns__ = null; 296 | } 297 | } 298 | 299 | var api = { 300 | before: function before(hook) { 301 | beforeHooks.push(hook); 302 | }, 303 | create: function create(config, renderFn, _single) { 304 | if (!isFunction(renderFn) && isUndef(_single)) { 305 | _single = renderFn; 306 | renderFn = null; 307 | } 308 | 309 | if (isUndef(_single)) { 310 | _single = single; 311 | } 312 | 313 | var ownerInstance = this; 314 | var isInVueInstance = !!ownerInstance.$on; 315 | var options = {}; 316 | 317 | if (isInVueInstance) { 318 | // Set parent to store router i18n ... 319 | options.parent = ownerInstance; 320 | if (!ownerInstance.__unwatchFns__) { 321 | ownerInstance.__unwatchFns__ = []; 322 | } 323 | } 324 | 325 | var renderData = parseRenderData(config, events); 326 | 327 | var component = null; 328 | 329 | processProps(ownerInstance, renderData, isInVueInstance, function (newProps) { 330 | component && component.$updateProps(newProps); 331 | }); 332 | processEvents(renderData, ownerInstance); 333 | process$(renderData); 334 | 335 | component = createComponent(renderData, renderFn, options, _single, ownerInstance); 336 | 337 | if (isInVueInstance) { 338 | ownerInstance.$on(eventBeforeDestroy, component.remove.bind(component)); 339 | } 340 | 341 | add(component); 342 | 343 | return component; 344 | } 345 | }; 346 | 347 | return api; 348 | } 349 | 350 | function install(Vue) { 351 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 352 | var _options$componentPre = options.componentPrefix, 353 | componentPrefix = _options$componentPre === undefined ? '' : _options$componentPre, 354 | _options$apiPrefix = options.apiPrefix, 355 | apiPrefix = _options$apiPrefix === undefined ? '$create-' : _options$apiPrefix; 356 | 357 | 358 | Vue.createAPI = function (Component, events, single) { 359 | if (isBoolean(events)) { 360 | single = events; 361 | events = []; 362 | } 363 | var api = apiCreator.call(this, Component, events, single); 364 | var createName = processComponentName(Component, { 365 | componentPrefix: componentPrefix, 366 | apiPrefix: apiPrefix 367 | }); 368 | Vue.prototype[createName] = Component.$create = api.create; 369 | return api; 370 | }; 371 | } 372 | 373 | function processComponentName(Component, options) { 374 | var componentPrefix = options.componentPrefix, 375 | apiPrefix = options.apiPrefix; 376 | 377 | var name = Component.name; 378 | assert(name, 'Component must have name while using create-api!'); 379 | var prefixReg = new RegExp('^' + escapeReg(componentPrefix), 'i'); 380 | var pureName = name.replace(prefixReg, ''); 381 | var camelizeName = '' + camelize('' + apiPrefix + pureName); 382 | return camelizeName; 383 | } 384 | 385 | var index = { 386 | install: install, 387 | batchDestroy: batchDestroy, 388 | instantiateComponent: instantiateComponent, 389 | version: '0.2.3' 390 | }; 391 | 392 | export default index; 393 | -------------------------------------------------------------------------------- /dist/vue-create-api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * vue-create-api v0.2.3 3 | * (c) 2025 ustbhuangyi 4 | * @license MIT 5 | */ 6 | (function (global, factory) { 7 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 8 | typeof define === 'function' && define.amd ? define(factory) : 9 | (global.VueCreateAPI = factory()); 10 | }(this, (function () { 'use strict'; 11 | 12 | var _extends = Object.assign || function (target) { 13 | for (var i = 1; i < arguments.length; i++) { 14 | var source = arguments[i]; 15 | 16 | for (var key in source) { 17 | if (Object.prototype.hasOwnProperty.call(source, key)) { 18 | target[key] = source[key]; 19 | } 20 | } 21 | } 22 | 23 | return target; 24 | }; 25 | 26 | var camelizeRE = /-(\w)/g; 27 | 28 | function camelize(str) { 29 | return (str + '').replace(camelizeRE, function (m, c) { 30 | return c ? c.toUpperCase() : ''; 31 | }); 32 | } 33 | 34 | function escapeReg(str, delimiter) { 35 | return (str + '').replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + (delimiter || '') + '-]', 'g'), '\\$&'); 36 | } 37 | 38 | function isBoolean(value) { 39 | return typeof value === 'boolean'; 40 | } 41 | 42 | function isUndef(value) { 43 | return value === undefined; 44 | } 45 | 46 | function isStr(value) { 47 | return typeof value === 'string'; 48 | } 49 | 50 | function isFunction(fn) { 51 | return typeof fn === 'function'; 52 | } 53 | 54 | function isArray(arr) { 55 | return Object.prototype.toString.call(arr) === '[object Array]'; 56 | } 57 | 58 | function assert(condition, msg) { 59 | if (!condition) { 60 | throw new Error("[vue-create-api error]: " + msg); 61 | } 62 | } 63 | 64 | function instantiateComponent(Vue, Component, data, renderFn, options) { 65 | var renderData = void 0; 66 | var childrenRenderFn = void 0; 67 | 68 | var instance = new Vue(_extends({}, options, { 69 | render: function render(createElement) { 70 | var children = childrenRenderFn && childrenRenderFn(createElement); 71 | if (children && !Array.isArray(children)) { 72 | children = [children]; 73 | } 74 | 75 | return createElement(Component, _extends({}, renderData), children || []); 76 | }, 77 | 78 | methods: { 79 | init: function init() { 80 | document.body.appendChild(this.$el); 81 | }, 82 | destroy: function destroy() { 83 | this.$destroy(); 84 | if (this.$el && this.$el.parentNode === document.body) { 85 | document.body.removeChild(this.$el); 86 | } 87 | } 88 | } 89 | })); 90 | instance.updateRenderData = function (data, render) { 91 | renderData = data; 92 | childrenRenderFn = render; 93 | }; 94 | instance.updateRenderData(data, renderFn); 95 | instance.$mount(); 96 | instance.init(); 97 | var component = instance.$children[0]; 98 | component.$updateProps = function (props) { 99 | _extends(renderData.props, props); 100 | instance.$forceUpdate(); 101 | }; 102 | return component; 103 | } 104 | 105 | function parseRenderData() { 106 | var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 107 | var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 108 | 109 | events = parseEvents(events); 110 | var props = _extends({}, data); 111 | var on = {}; 112 | for (var name in events) { 113 | if (events.hasOwnProperty(name)) { 114 | var handlerName = events[name]; 115 | if (props[handlerName]) { 116 | on[name] = props[handlerName]; 117 | delete props[handlerName]; 118 | } 119 | } 120 | } 121 | return { 122 | props: props, 123 | on: on 124 | }; 125 | } 126 | 127 | function parseEvents(events) { 128 | var parsedEvents = {}; 129 | events.forEach(function (name) { 130 | parsedEvents[name] = camelize('on-' + name); 131 | }); 132 | return parsedEvents; 133 | } 134 | 135 | var instances = []; 136 | 137 | function add(component) { 138 | var ins = void 0; 139 | var len = instances.length; 140 | for (var i = 0; i < len; i += 1) { 141 | ins = instances[i]; 142 | if (ins === component) { 143 | return; 144 | } 145 | } 146 | instances.push(component); 147 | } 148 | 149 | function remove(component) { 150 | var ins = void 0; 151 | var len = instances.length; 152 | for (var i = 0; i < len; i += 1) { 153 | ins = instances[i]; 154 | if (ins === component) { 155 | instances.splice(i, 1); 156 | } 157 | } 158 | } 159 | 160 | function batchDestroy(filter) { 161 | var hasFilter = isFunction(filter); 162 | var instancesCopy = instances.slice(); 163 | var _instances = hasFilter ? filter(instancesCopy) : instancesCopy; 164 | if (!isArray(_instances)) { 165 | return; 166 | } 167 | _instances.forEach(function (ins) { 168 | if (ins && isFunction(ins.remove)) { 169 | ins.remove(); 170 | } 171 | }); 172 | } 173 | 174 | var eventBeforeDestroy = 'hook:beforeDestroy'; 175 | 176 | function apiCreator(Component) { 177 | var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 178 | var single = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; 179 | 180 | var Vue = this; 181 | var singleMap = {}; 182 | var beforeHooks = []; 183 | 184 | function createComponent(renderData, renderFn, options, single, ownerInstance) { 185 | beforeHooks.forEach(function (before) { 186 | before(renderData, renderFn, single); 187 | }); 188 | var ownerInsUid = options.parent ? options.parent._uid : -1; 189 | 190 | var _ref = singleMap[ownerInsUid] ? singleMap[ownerInsUid] : {}, 191 | comp = _ref.comp, 192 | ins = _ref.ins; 193 | 194 | if (single && comp && ins) { 195 | ins.updateRenderData(renderData, renderFn); 196 | ins.$forceUpdate(); 197 | return comp; 198 | } 199 | var component = instantiateComponent(Vue, Component, renderData, renderFn, options); 200 | var instance = component.$parent; 201 | var originRemove = component.remove; 202 | var isInVueInstance = !!ownerInstance.$on; 203 | 204 | component.remove = function () { 205 | if (isInVueInstance) { 206 | cancelWatchProps(ownerInstance); 207 | } 208 | if (single) { 209 | if (!singleMap[ownerInsUid]) { 210 | return; 211 | } 212 | singleMap[ownerInsUid] = null; 213 | } 214 | originRemove && originRemove.apply(this, arguments); 215 | instance.destroy(); 216 | remove(component); 217 | }; 218 | 219 | var originShow = component.show; 220 | component.show = function () { 221 | originShow && originShow.apply(this, arguments); 222 | return this; 223 | }; 224 | 225 | var originHide = component.hide; 226 | component.hide = function () { 227 | originHide && originHide.apply(this, arguments); 228 | return this; 229 | }; 230 | 231 | if (single) { 232 | singleMap[ownerInsUid] = { 233 | comp: component, 234 | ins: instance 235 | }; 236 | } 237 | return component; 238 | } 239 | 240 | function processProps(ownerInstance, renderData, isInVueInstance, onChange) { 241 | var $props = renderData.props.$props; 242 | if ($props) { 243 | delete renderData.props.$props; 244 | 245 | var watchKeys = []; 246 | var watchPropKeys = []; 247 | Object.keys($props).forEach(function (key) { 248 | var propKey = $props[key]; 249 | if (isStr(propKey) && propKey in ownerInstance) { 250 | // get instance value 251 | renderData.props[key] = ownerInstance[propKey]; 252 | watchKeys.push(key); 253 | watchPropKeys.push(propKey); 254 | } else { 255 | renderData.props[key] = propKey; 256 | } 257 | }); 258 | if (isInVueInstance) { 259 | var unwatchFn = ownerInstance.$watch(function () { 260 | var props = {}; 261 | watchKeys.forEach(function (key, i) { 262 | props[key] = ownerInstance[watchPropKeys[i]]; 263 | }); 264 | return props; 265 | }, onChange); 266 | ownerInstance.__unwatchFns__.push(unwatchFn); 267 | } 268 | } 269 | } 270 | 271 | function processEvents(renderData, ownerInstance) { 272 | var $events = renderData.props.$events; 273 | if ($events) { 274 | delete renderData.props.$events; 275 | 276 | Object.keys($events).forEach(function (event) { 277 | var eventHandler = $events[event]; 278 | if (typeof eventHandler === 'string') { 279 | eventHandler = ownerInstance[eventHandler]; 280 | } 281 | renderData.on[event] = eventHandler; 282 | }); 283 | } 284 | } 285 | 286 | function process$(renderData) { 287 | var props = renderData.props; 288 | Object.keys(props).forEach(function (prop) { 289 | if (prop.charAt(0) === '$') { 290 | renderData[prop.slice(1)] = props[prop]; 291 | delete props[prop]; 292 | } 293 | }); 294 | } 295 | 296 | function cancelWatchProps(ownerInstance) { 297 | if (ownerInstance.__unwatchFns__) { 298 | ownerInstance.__unwatchFns__.forEach(function (unwatchFn) { 299 | unwatchFn(); 300 | }); 301 | ownerInstance.__unwatchFns__ = null; 302 | } 303 | } 304 | 305 | var api = { 306 | before: function before(hook) { 307 | beforeHooks.push(hook); 308 | }, 309 | create: function create(config, renderFn, _single) { 310 | if (!isFunction(renderFn) && isUndef(_single)) { 311 | _single = renderFn; 312 | renderFn = null; 313 | } 314 | 315 | if (isUndef(_single)) { 316 | _single = single; 317 | } 318 | 319 | var ownerInstance = this; 320 | var isInVueInstance = !!ownerInstance.$on; 321 | var options = {}; 322 | 323 | if (isInVueInstance) { 324 | // Set parent to store router i18n ... 325 | options.parent = ownerInstance; 326 | if (!ownerInstance.__unwatchFns__) { 327 | ownerInstance.__unwatchFns__ = []; 328 | } 329 | } 330 | 331 | var renderData = parseRenderData(config, events); 332 | 333 | var component = null; 334 | 335 | processProps(ownerInstance, renderData, isInVueInstance, function (newProps) { 336 | component && component.$updateProps(newProps); 337 | }); 338 | processEvents(renderData, ownerInstance); 339 | process$(renderData); 340 | 341 | component = createComponent(renderData, renderFn, options, _single, ownerInstance); 342 | 343 | if (isInVueInstance) { 344 | ownerInstance.$on(eventBeforeDestroy, component.remove.bind(component)); 345 | } 346 | 347 | add(component); 348 | 349 | return component; 350 | } 351 | }; 352 | 353 | return api; 354 | } 355 | 356 | function install(Vue) { 357 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 358 | var _options$componentPre = options.componentPrefix, 359 | componentPrefix = _options$componentPre === undefined ? '' : _options$componentPre, 360 | _options$apiPrefix = options.apiPrefix, 361 | apiPrefix = _options$apiPrefix === undefined ? '$create-' : _options$apiPrefix; 362 | 363 | 364 | Vue.createAPI = function (Component, events, single) { 365 | if (isBoolean(events)) { 366 | single = events; 367 | events = []; 368 | } 369 | var api = apiCreator.call(this, Component, events, single); 370 | var createName = processComponentName(Component, { 371 | componentPrefix: componentPrefix, 372 | apiPrefix: apiPrefix 373 | }); 374 | Vue.prototype[createName] = Component.$create = api.create; 375 | return api; 376 | }; 377 | } 378 | 379 | function processComponentName(Component, options) { 380 | var componentPrefix = options.componentPrefix, 381 | apiPrefix = options.apiPrefix; 382 | 383 | var name = Component.name; 384 | assert(name, 'Component must have name while using create-api!'); 385 | var prefixReg = new RegExp('^' + escapeReg(componentPrefix), 'i'); 386 | var pureName = name.replace(prefixReg, ''); 387 | var camelizeName = '' + camelize('' + apiPrefix + pureName); 388 | return camelizeName; 389 | } 390 | 391 | var index = { 392 | install: install, 393 | batchDestroy: batchDestroy, 394 | instantiateComponent: instantiateComponent, 395 | version: '0.2.3' 396 | }; 397 | 398 | return index; 399 | 400 | }))); 401 | -------------------------------------------------------------------------------- /dist/vue-create-api.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * vue-create-api v0.2.3 3 | * (c) 2025 ustbhuangyi 4 | * @license MIT 5 | */ 6 | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):e.VueCreateAPI=n()}(this,function(){"use strict";var p=Object.assign||function(e){for(var n=1;n|:\\"+(i||"")+"-]","g"),"\\$&"),"i"),c=o.replace(a,"");return""+u(""+r+c)}(e,{componentPrefix:a,apiPrefix:c});return i.prototype[o]=e.$create=r.create,r}},batchDestroy:function(e){var n,t=g(e),r=E.slice(),o=t?e(r):r;n=o,"[object Array]"===Object.prototype.toString.call(n)&&o.forEach(function(e){e&&g(e.remove)&&e.remove()})},instantiateComponent:b,version:"0.2.3"}}); -------------------------------------------------------------------------------- /examples/dialog/app.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import Vue from 'vue' 3 | import App from './components/App.vue' 4 | import Dialog from './components/dialog.vue' 5 | import CreateAPI from 'create-api' 6 | 7 | Vue.use(CreateAPI, { 8 | componentPrefix: 'z-' 9 | }) 10 | 11 | Vue.createAPI(Dialog, true) 12 | 13 | Vue.config.debug = true 14 | 15 | Dialog.$create({ 16 | $props: { 17 | title: 'Hello', 18 | content: 'I am from pure JS' 19 | } 20 | }).show() 21 | 22 | new Vue({ 23 | el: '#app', 24 | render: h => h(App) 25 | }) 26 | 27 | -------------------------------------------------------------------------------- /examples/dialog/components/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 37 | 39 | -------------------------------------------------------------------------------- /examples/dialog/components/child.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 35 | 36 | 39 | -------------------------------------------------------------------------------- /examples/dialog/components/dialog.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 39 | 83 | -------------------------------------------------------------------------------- /examples/dialog/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dialog example 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/global.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 3 | color: #2c3e50; 4 | } 5 | 6 | ul { 7 | line-height: 1.5em; 8 | padding-left: 1.5em; 9 | } 10 | 11 | a { 12 | color: #7f8c8d; 13 | text-decoration: none; 14 | } 15 | 16 | a:hover { 17 | color: #4fc08d; 18 | } 19 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | create-api examples 6 | 7 | 8 | 9 |

create-api examples

10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const webpack = require('webpack') 3 | const webpackDevMiddleware = require('webpack-dev-middleware') 4 | const webpackHotMiddleware = require('webpack-hot-middleware') 5 | const WebpackConfig = require('./webpack.config') 6 | 7 | const app = express() 8 | const compiler = webpack(WebpackConfig) 9 | 10 | app.use(webpackDevMiddleware(compiler, { 11 | publicPath: '/__build__/', 12 | stats: { 13 | colors: true, 14 | chunks: false 15 | } 16 | })) 17 | 18 | app.use(webpackHotMiddleware(compiler)) 19 | 20 | app.use(express.static(__dirname)) 21 | 22 | const port = process.env.PORT || 8080 23 | module.exports = app.listen(port, () => { 24 | console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`) 25 | }) 26 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 5 | 6 | module.exports = { 7 | mode: 'development', 8 | 9 | entry: fs.readdirSync(__dirname).reduce((entries, dir) => { 10 | const fullDir = path.join(__dirname, dir) 11 | const entry = path.join(fullDir, 'app.js') 12 | if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) { 13 | entries[dir] = ['webpack-hot-middleware/client', entry] 14 | } 15 | 16 | return entries 17 | }, {}), 18 | 19 | output: { 20 | path: path.join(__dirname, '__build__'), 21 | filename: '[name].js', 22 | chunkFilename: '[id].chunk.js', 23 | publicPath: '/__build__/' 24 | }, 25 | 26 | module: { 27 | rules: [ 28 | { test: /\.js$/, exclude: /node_modules/, use: ['babel-loader'] }, 29 | { test: /\.vue$/, use: ['vue-loader'] }, 30 | { test: /\.css$/, use: ['vue-style-loader', 'css-loader'] } 31 | ] 32 | }, 33 | 34 | resolve: { 35 | alias: { 36 | 'create-api': path.resolve(__dirname, '../src/index.js') 37 | } 38 | }, 39 | 40 | optimization: { 41 | splitChunks: { 42 | cacheGroups: { 43 | vendors: { 44 | name: 'shared', 45 | filename: 'shared.js', 46 | chunks: 'initial' 47 | } 48 | } 49 | } 50 | }, 51 | 52 | plugins: [ 53 | new VueLoaderPlugin(), 54 | new webpack.HotModuleReplacementPlugin(), 55 | new webpack.NoEmitOnErrorsPlugin() 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /examples/webpack.test.js: -------------------------------------------------------------------------------- 1 | // This is the webpack config used for unit tests. 2 | 3 | var merge = require('webpack-merge') 4 | var baseConfig = require('./webpack.config.js') 5 | 6 | var webpackConfig = merge(baseConfig, { 7 | // use inline sourcemap for karma-sourcemap-loader 8 | devtool: '#inline-source-map' 9 | }) 10 | 11 | module.exports = webpackConfig 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-create-api", 3 | "version": "0.2.4", 4 | "description": "A Vue plugin which make Vue component invocated by API.", 5 | "module": "dist/vue-create-api.esm.js", 6 | "main": "dist/vue-create-api.js", 7 | "types": "types/index.d.ts", 8 | "files": [ 9 | "dist", 10 | "types" 11 | ], 12 | "scripts": { 13 | "dev": "node examples/server.js", 14 | "build": "node build/build.js", 15 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js", 16 | "codecov": "codecov", 17 | "test": "npm run unit && npm run codecov" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/cube-ui/create-api.git" 22 | }, 23 | "keywords": [ 24 | "create-api", 25 | "cube-ui", 26 | "Vue", 27 | "component" 28 | ], 29 | "author": "ustbhuangyi", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/cube-ui/create-api/issues" 33 | }, 34 | "homepage": "https://github.com/cube-ui/create-api#readme", 35 | "devDependencies": { 36 | "babel-core": "^6.22.1", 37 | "babel-eslint": "^8.2.6", 38 | "babel-loader": "^7.1.2", 39 | "babel-plugin-external-helpers": "^6.22.0", 40 | "babel-plugin-istanbul": "^5.0.1", 41 | "babel-plugin-transform-object-assign": "^6.22.0", 42 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 43 | "babel-polyfill": "^6.22.0", 44 | "babel-preset-env": "^1.5.1", 45 | "chai": "^4.2.0", 46 | "codecov": "^3.1.0", 47 | "cross-env": "^5.2.0", 48 | "cross-spawn": "^5.0.1", 49 | "css-loader": "^0.28.7", 50 | "eslint": "^4.15.0", 51 | "eslint-config-standard": "^10.2.1", 52 | "eslint-friendly-formatter": "^3.0.0", 53 | "eslint-loader": "^1.7.1", 54 | "eslint-plugin-import": "^2.7.0", 55 | "eslint-plugin-node": "^5.2.0", 56 | "eslint-plugin-promise": "^3.4.0", 57 | "eslint-plugin-standard": "^3.0.1", 58 | "eslint-plugin-vue": "^4.0.0", 59 | "express": "^4.14.1", 60 | "karma": "^3.0.0", 61 | "karma-chai": "^0.1.0", 62 | "karma-chrome-launcher": "^2.2.0", 63 | "karma-coverage": "^1.1.2", 64 | "karma-mocha": "^1.3.0", 65 | "karma-phantomjs-launcher": "^1.0.4", 66 | "karma-sinon-chai": "^2.0.2", 67 | "karma-sourcemap-loader": "^0.3.7", 68 | "karma-spec-reporter": "0.0.32", 69 | "karma-webpack": "^4.0.0-rc.2", 70 | "mocha": "^5.2.0", 71 | "rollup": "^0.65.0", 72 | "rollup-plugin-babel": "^3.0.7", 73 | "rollup-plugin-replace": "^2.0.0", 74 | "rollup-watch": "^4.3.1", 75 | "sinon": "^6.3.5", 76 | "sinon-chai": "^3.2.0", 77 | "uglify-js": "^3.1.2", 78 | "vue": "^2.5.16", 79 | "vue-loader": "^15.2.1", 80 | "vue-template-compiler": "^2.5.16", 81 | "vuepress": "^0.10.0", 82 | "vuepress-theme-vue": "^1.0.3", 83 | "webpack": "^4.20.2", 84 | "webpack-dev-middleware": "^1.10.0", 85 | "webpack-hot-middleware": "^2.19.1" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/cache.js: -------------------------------------------------------------------------------- 1 | import { isFunction, isArray } from './util' 2 | 3 | const instances = [] 4 | 5 | export function add(component) { 6 | let ins 7 | const len = instances.length 8 | for (let i = 0; i < len; i += 1) { 9 | ins = instances[i] 10 | if (ins === component) { 11 | return 12 | } 13 | } 14 | instances.push(component) 15 | } 16 | 17 | export function remove(component) { 18 | let ins 19 | const len = instances.length 20 | for (let i = 0; i < len; i += 1) { 21 | ins = instances[i] 22 | if (ins === component) { 23 | instances.splice(i, 1) 24 | } 25 | } 26 | } 27 | 28 | export function batchDestroy(filter) { 29 | const hasFilter = isFunction(filter) 30 | const instancesCopy = instances.slice() 31 | const _instances = hasFilter ? filter(instancesCopy) : instancesCopy 32 | if (!isArray(_instances)) { 33 | return 34 | } 35 | _instances.forEach(ins => { 36 | if (ins && isFunction(ins.remove)) { 37 | ins.remove() 38 | } 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/creator.js: -------------------------------------------------------------------------------- 1 | import instantiateComponent from './instantiate' 2 | import parseRenderData from './parse' 3 | import { isFunction, isUndef, isStr } from './util' 4 | import * as cache from './cache' 5 | 6 | const eventBeforeDestroy = 'hook:beforeDestroy' 7 | 8 | export default function apiCreator(Component, events = [], single = false) { 9 | let Vue = this 10 | let singleMap = {} 11 | const beforeHooks = [] 12 | 13 | function createComponent(renderData, renderFn, options, single, ownerInstance) { 14 | beforeHooks.forEach((before) => { 15 | before(renderData, renderFn, single) 16 | }) 17 | const ownerInsUid = options.parent ? options.parent._uid : -1 18 | const {comp, ins} = singleMap[ownerInsUid] ? singleMap[ownerInsUid] : {} 19 | if (single && comp && ins) { 20 | ins.updateRenderData(renderData, renderFn) 21 | ins.$forceUpdate() 22 | return comp 23 | } 24 | const component = instantiateComponent(Vue, Component, renderData, renderFn, options) 25 | const instance = component.$parent 26 | const originRemove = component.remove 27 | const isInVueInstance = !!ownerInstance.$on 28 | 29 | component.remove = function () { 30 | if (isInVueInstance) { 31 | cancelWatchProps(ownerInstance) 32 | } 33 | if (single) { 34 | if (!singleMap[ownerInsUid]) { 35 | return 36 | } 37 | singleMap[ownerInsUid] = null 38 | } 39 | originRemove && originRemove.apply(this, arguments) 40 | instance.destroy() 41 | cache.remove(component) 42 | } 43 | 44 | const originShow = component.show 45 | component.show = function () { 46 | originShow && originShow.apply(this, arguments) 47 | return this 48 | } 49 | 50 | const originHide = component.hide 51 | component.hide = function () { 52 | originHide && originHide.apply(this, arguments) 53 | return this 54 | } 55 | 56 | if (single) { 57 | singleMap[ownerInsUid] = { 58 | comp: component, 59 | ins: instance 60 | } 61 | } 62 | return component 63 | } 64 | 65 | function processProps(ownerInstance, renderData, isInVueInstance, onChange) { 66 | const $props = renderData.props.$props 67 | if ($props) { 68 | delete renderData.props.$props 69 | 70 | const watchKeys = [] 71 | const watchPropKeys = [] 72 | Object.keys($props).forEach((key) => { 73 | const propKey = $props[key] 74 | if (isStr(propKey) && propKey in ownerInstance) { 75 | // get instance value 76 | renderData.props[key] = ownerInstance[propKey] 77 | watchKeys.push(key) 78 | watchPropKeys.push(propKey) 79 | } else { 80 | renderData.props[key] = propKey 81 | } 82 | }) 83 | if (isInVueInstance) { 84 | const unwatchFn = ownerInstance.$watch(function () { 85 | const props = {} 86 | watchKeys.forEach((key, i) => { 87 | props[key] = ownerInstance[watchPropKeys[i]] 88 | }) 89 | return props 90 | }, onChange) 91 | ownerInstance.__unwatchFns__.push(unwatchFn) 92 | } 93 | } 94 | } 95 | 96 | function processEvents(renderData, ownerInstance 97 | ) { 98 | const $events = renderData.props.$events 99 | if ($events) { 100 | delete renderData.props.$events 101 | 102 | Object.keys($events).forEach((event) => { 103 | let eventHandler = $events[event] 104 | if (typeof eventHandler === 'string') { 105 | eventHandler = ownerInstance[eventHandler] 106 | } 107 | renderData.on[event] = eventHandler 108 | }) 109 | } 110 | } 111 | 112 | function process$(renderData) { 113 | const props = renderData.props 114 | Object.keys(props).forEach((prop) => { 115 | if (prop.charAt(0) === '$') { 116 | renderData[prop.slice(1)] = props[prop] 117 | delete props[prop] 118 | } 119 | }) 120 | } 121 | 122 | function cancelWatchProps(ownerInstance) { 123 | if (ownerInstance.__unwatchFns__) { 124 | ownerInstance.__unwatchFns__.forEach((unwatchFn) => { 125 | unwatchFn() 126 | }) 127 | ownerInstance.__unwatchFns__ = null 128 | } 129 | } 130 | 131 | const api = { 132 | before(hook) { 133 | beforeHooks.push(hook) 134 | }, 135 | create(config, renderFn, _single) { 136 | if (!isFunction(renderFn) && isUndef(_single)) { 137 | _single = renderFn 138 | renderFn = null 139 | } 140 | 141 | if (isUndef(_single)) { 142 | _single = single 143 | } 144 | 145 | const ownerInstance = this 146 | const isInVueInstance = !!ownerInstance.$on 147 | let options = {} 148 | 149 | if (isInVueInstance) { 150 | // Set parent to store router i18n ... 151 | options.parent = ownerInstance 152 | if (!ownerInstance.__unwatchFns__) { 153 | ownerInstance.__unwatchFns__ = [] 154 | } 155 | } 156 | 157 | const renderData = parseRenderData(config, events) 158 | 159 | let component = null 160 | 161 | processProps(ownerInstance, renderData, isInVueInstance, (newProps) => { 162 | component && component.$updateProps(newProps) 163 | }) 164 | processEvents(renderData, ownerInstance) 165 | process$(renderData) 166 | 167 | component = createComponent(renderData, renderFn, options, _single, ownerInstance) 168 | 169 | if (isInVueInstance) { 170 | ownerInstance.$on(eventBeforeDestroy, component.remove.bind(component)) 171 | } 172 | 173 | cache.add(component) 174 | 175 | return component 176 | } 177 | } 178 | 179 | return api 180 | } 181 | -------------------------------------------------------------------------------- /src/debug.js: -------------------------------------------------------------------------------- 1 | export function warn(msg) { 2 | console.error(`[vue-create-api warn]: ${msg}`) 3 | } 4 | 5 | export function assert(condition, msg) { 6 | if (!condition) { 7 | throw new Error(`[vue-create-api error]: ${msg}`) 8 | } 9 | } 10 | 11 | export function tip(msg) { 12 | console.warn(`[vue-create-api tip]: ${msg}`) 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { camelize, escapeReg, isBoolean } from './util' 2 | import { assert, warn } from './debug' 3 | import apiCreator from './creator' 4 | import instantiateComponent from './instantiate' 5 | import { batchDestroy } from './cache' 6 | 7 | function install(Vue, options = {}) { 8 | const {componentPrefix = '', apiPrefix = '$create-'} = options 9 | 10 | Vue.createAPI = function (Component, events, single) { 11 | if (isBoolean(events)) { 12 | single = events 13 | events = [] 14 | } 15 | const api = apiCreator.call(this, Component, events, single) 16 | const createName = processComponentName(Component, { 17 | componentPrefix, 18 | apiPrefix, 19 | }) 20 | Vue.prototype[createName] = Component.$create = api.create 21 | return api 22 | } 23 | } 24 | 25 | function processComponentName(Component, options) { 26 | const {componentPrefix, apiPrefix} = options 27 | const name = Component.name 28 | assert(name, 'Component must have name while using create-api!') 29 | const prefixReg = new RegExp(`^${escapeReg(componentPrefix)}`, 'i') 30 | const pureName = name.replace(prefixReg, '') 31 | let camelizeName = `${camelize(`${apiPrefix}${pureName}`)}` 32 | return camelizeName 33 | } 34 | 35 | export default { 36 | install, 37 | batchDestroy, 38 | instantiateComponent, 39 | version: '__VERSION__' 40 | } 41 | -------------------------------------------------------------------------------- /src/instantiate.js: -------------------------------------------------------------------------------- 1 | export default function instantiateComponent(Vue, Component, data, renderFn, options) { 2 | let renderData 3 | let childrenRenderFn 4 | 5 | const instance = new Vue({ 6 | ...options, 7 | render(createElement) { 8 | let children = childrenRenderFn && childrenRenderFn(createElement) 9 | if (children && !Array.isArray(children)) { 10 | children = [children] 11 | } 12 | 13 | return createElement(Component, {...renderData}, children || []) 14 | }, 15 | methods: { 16 | init() { 17 | document.body.appendChild(this.$el) 18 | }, 19 | destroy() { 20 | this.$destroy() 21 | if (this.$el && this.$el.parentNode === document.body) { 22 | document.body.removeChild(this.$el) 23 | } 24 | } 25 | } 26 | }) 27 | instance.updateRenderData = function (data, render) { 28 | renderData = data 29 | childrenRenderFn = render 30 | } 31 | instance.updateRenderData(data, renderFn) 32 | instance.$mount() 33 | instance.init() 34 | const component = instance.$children[0] 35 | component.$updateProps = function (props) { 36 | Object.assign(renderData.props, props) 37 | instance.$forceUpdate() 38 | } 39 | return component 40 | } 41 | -------------------------------------------------------------------------------- /src/parse.js: -------------------------------------------------------------------------------- 1 | import { camelize } from './util' 2 | 3 | export default function parseRenderData(data = {}, events = {}) { 4 | events = parseEvents(events) 5 | const props = {...data} 6 | const on = {} 7 | for (const name in events) { 8 | if (events.hasOwnProperty(name)) { 9 | const handlerName = events[name] 10 | if (props[handlerName]) { 11 | on[name] = props[handlerName] 12 | delete props[handlerName] 13 | } 14 | } 15 | } 16 | return { 17 | props, 18 | on 19 | } 20 | } 21 | 22 | function parseEvents(events) { 23 | const parsedEvents = {} 24 | events.forEach((name) => { 25 | parsedEvents[name] = camelize(`on-${name}`) 26 | }) 27 | return parsedEvents 28 | } 29 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | const camelizeRE = /-(\w)/g 2 | 3 | export function camelize(str) { 4 | return (str + '').replace(camelizeRE, function (m, c) { 5 | return c ? c.toUpperCase() : '' 6 | }) 7 | } 8 | 9 | export function escapeReg(str, delimiter) { 10 | return (str + '').replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + (delimiter || '') + '-]', 'g'), '\\$&') 11 | } 12 | 13 | export function isBoolean(value) { 14 | return typeof value === 'boolean' 15 | } 16 | 17 | export function isObject(obj) { 18 | return obj !== null && typeof obj === 'object' 19 | } 20 | 21 | export function isUndef(value) { 22 | return value === undefined 23 | } 24 | 25 | export function isStr(value) { 26 | return typeof value === 'string' 27 | } 28 | 29 | export function isFunction(fn) { 30 | return typeof fn === 'function' 31 | } 32 | 33 | export function isArray(arr) { 34 | return Object.prototype.toString.call(arr) === '[object Array]' 35 | } 36 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true 7 | }, 8 | "rules": { 9 | "no-unused-expressions": 0 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/unit/components/app.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 43 | 45 | -------------------------------------------------------------------------------- /test/unit/components/dialog.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 40 | 84 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import CreateAPI from '../../src/index.js' 3 | import Dialog from './components/dialog.vue' 4 | import App from './components/app.vue' 5 | 6 | Vue.config.productionTip = false 7 | 8 | describe('create api 单元测试', function () { 9 | before(() => { 10 | Vue.use(CreateAPI, { 11 | componentPrefix: 'z-' 12 | }) 13 | }) 14 | 15 | describe('#Vue.use', function() { 16 | it('expect to add createDialog API', function () { 17 | expect(Vue.createAPI).to.be.a('function') 18 | 19 | Vue.createAPI(Dialog, true) 20 | 21 | expect(Vue.prototype.$createDialog).to.be.a('function') 22 | expect(Dialog.$create).to.be.a('function') 23 | }) 24 | }) 25 | 26 | describe('#CreateAPI in pure JS', function () { 27 | let dialog 28 | let api 29 | before(() => { 30 | api = Vue.createAPI(Dialog, ['click'], true) 31 | }) 32 | after(() => { 33 | dialog.$parent.destroy() 34 | }) 35 | 36 | // 测试正确渲染内容 37 | it('expect to render correct content', function () { 38 | dialog = Dialog.$create({ 39 | title: 'Hello', 40 | content: 'I am from pure JS1' 41 | }) 42 | 43 | dialog.show() 44 | dialog.hide() 45 | 46 | let content = document.querySelector('.dialog .content') 47 | expect(content.textContent).to.equal('I am from pure JS1') 48 | }) 49 | 50 | // 测试 beforeHooks 能够正常执行 51 | it('expect to execuate beforeHooks', function () { 52 | const fake = sinon.fake() 53 | 54 | api.before(fake) 55 | 56 | dialog = Dialog.$create() 57 | 58 | expect(fake).to.be.called 59 | }) 60 | 61 | // 测试配置项支持 $event 62 | it('expect config options to support $props/$event', function(done) { 63 | dialog = Dialog.$create({ 64 | $props: { 65 | title: 'Hello', 66 | content: 'I am from pure JS1' 67 | }, 68 | $events: { 69 | change: () => {} 70 | } 71 | }) 72 | 73 | dialog.$nextTick(() => { 74 | expect(Object.keys(dialog.$listeners)).to.include('change') 75 | done() 76 | }) 77 | }) 78 | 79 | // 测试配置项支持 on* 形式指定 事件回调 80 | it(`expect config options to support 'on'`, function(done) { 81 | dialog = Dialog.$create({ 82 | title: 'Hello', 83 | content: 'I am from pure JS2', 84 | onClick: () => {}, 85 | }) 86 | 87 | dialog.$nextTick(() => { 88 | expect(Object.keys(dialog.$listeners)).to.include('click') 89 | 90 | let content = document.querySelector('.dialog .content') 91 | expect(content.textContent).to.equal('I am from pure JS2') 92 | done() 93 | }) 94 | }) 95 | 96 | // 测试配置项支持任何 Vue 配置 97 | it(`expect config options to support $xx`, function(done) { 98 | dialog = Dialog.$create({ 99 | $class: ['my-class'], 100 | }) 101 | 102 | dialog.$nextTick(() => { 103 | const classList = Array.prototype.slice.apply(dialog.$el.classList) 104 | expect(classList).to.include('my-class') 105 | done() 106 | }) 107 | }) 108 | }) 109 | 110 | describe('#createAPI in Vue instance', function() { 111 | let app 112 | before(() => { 113 | Vue.createAPI(Dialog, true) 114 | 115 | const instance = new Vue({ 116 | render: h => h(App), 117 | components: { App } 118 | }).$mount() 119 | 120 | document.body.appendChild(instance.$el) 121 | 122 | app = instance.$children[0] 123 | }) 124 | 125 | it('expect to update when $props in ownInstance change', function(done) { 126 | app.showDialog() 127 | 128 | app.$nextTick(() => { 129 | let text = document.querySelector('.dialog .content').textContent 130 | expect(text).to.equal('I am from App') 131 | 132 | app.changeContent() 133 | app.$nextTick(() => { 134 | 135 | text = document.querySelector('.dialog .content').textContent 136 | expect(text).to.equal('I am from App and content changed!') 137 | 138 | done() 139 | }) 140 | }) 141 | }) 142 | 143 | it('expect to remove dom before destory', function(done) { 144 | app.showDialog() 145 | 146 | app.$nextTick(() => { 147 | app.$parent.$destroy() 148 | 149 | expect(document.querySelector('.dialog')).to.be.null 150 | 151 | done() 152 | }) 153 | }) 154 | }) 155 | 156 | describe('#Single mode', function() { 157 | let app 158 | before(() => { 159 | const instance = new Vue({ 160 | render: h => h(App), 161 | components: { App } 162 | }).$mount() 163 | 164 | document.body.appendChild(instance.$el) 165 | 166 | app = instance.$children[0] 167 | }) 168 | 169 | // 测试单例模式 返回同一个实例 170 | it('expect to return the same components in single mode', function() { 171 | Vue.createAPI(Dialog, true) 172 | const dialog1 = app.showDialog() 173 | const dialog2 = app.showAnotherDialog() 174 | expect(dialog1 === dialog2).to.be.true 175 | 176 | dialog1.$parent.destroy() 177 | }) 178 | // 测试非单例模式 返回多个实例 179 | it('expect to return different components when not in single mode', function(done) { 180 | Vue.createAPI(Dialog, false) 181 | const dialog1 = app.showDialog() 182 | const dialog2 = app.showAnotherDialog() 183 | expect(dialog1 === dialog2).to.be.false 184 | 185 | Vue.nextTick(() => { 186 | const dialogs = document.querySelectorAll('.dialog') 187 | const length = Array.prototype.slice.apply(dialogs).length 188 | expect(length).to.equal(2) 189 | 190 | done() 191 | }) 192 | }) 193 | }) 194 | 195 | describe('#Batch destroy', function() { 196 | before(() => { 197 | CreateAPI.batchDestroy() 198 | Vue.createAPI(Dialog, ['click'], false) 199 | }) 200 | 201 | // 测试batchDestroy 销毁非this调用组件 202 | it('expect to clear all instances in batch destory', function(done) { 203 | const cls = 'dialog-batch-destroy' 204 | const dialog1 = Dialog.$create({ 205 | title: 'Hello', 206 | content: 'I am from pure JS1', 207 | $class: cls 208 | }) 209 | const dialog2 = Dialog.$create({ 210 | title: 'Hello', 211 | content: 'I am from pure JS2', 212 | $class: cls 213 | }) 214 | 215 | dialog1.show() 216 | dialog2.show() 217 | 218 | Vue.nextTick(() => { 219 | const dialogs = document.querySelectorAll(`.${cls}`) 220 | const length = Array.prototype.slice.apply(dialogs).length 221 | expect(length).to.equal(2) 222 | 223 | CreateAPI.batchDestroy() 224 | 225 | { 226 | const dialogs = document.querySelectorAll(`.${cls}`) 227 | const length = Array.prototype.slice.apply(dialogs).length 228 | expect(length).to.equal(0) 229 | 230 | dialog1.remove() 231 | dialog2.remove() 232 | 233 | done() 234 | } 235 | }) 236 | }) 237 | 238 | // 测试batchDestroy filter功能 239 | it('expect to return a filtered instances in batch destory', function(done) { 240 | const cls = 'dialog-batch-destroy-filter' 241 | const dialog1 = Dialog.$create({ 242 | title: 'Hello', 243 | content: 'I am from pure JS1', 244 | $class: cls 245 | }) 246 | const dialog2 = Dialog.$create({ 247 | title: 'Hello', 248 | content: 'I am from pure JS2', 249 | $class: cls 250 | }) 251 | 252 | dialog1.show() 253 | dialog2.show() 254 | 255 | Vue.nextTick(() => { 256 | const dialogs = document.querySelectorAll(`.${cls}`) 257 | const length = Array.prototype.slice.apply(dialogs).length 258 | expect(length).to.equal(2) 259 | 260 | CreateAPI.batchDestroy((instances) => { 261 | return instances.filter(ins => ins.content === 'I am from pure JS2') 262 | }) 263 | 264 | { 265 | const dialogs = document.querySelectorAll(`.${cls}`) 266 | const length = Array.prototype.slice.apply(dialogs).length 267 | expect(length).to.equal(1) 268 | 269 | dialog1.remove() 270 | dialog2.remove() 271 | 272 | done() 273 | } 274 | }) 275 | }) 276 | }) 277 | }) -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/2.0/config/configuration-file.html 3 | 4 | const webpackConfig = require('../../examples/webpack.test.js') 5 | 6 | module.exports = function (config) { 7 | config.set({ 8 | browsers: ['PhantomJS'], 9 | frameworks: ['mocha', 'sinon-chai'], 10 | reporters: ['spec', 'coverage'], 11 | coverageReporter: { 12 | dir: './coverage', 13 | reporters: [ 14 | { type: 'lcov', subdir: '.' }, 15 | { type: 'text-summary' } 16 | ] 17 | }, 18 | files: [ 19 | './index.js' 20 | ], 21 | preprocessors: { 22 | './index.js': ['webpack', 'sourcemap'] 23 | }, 24 | webpack: webpackConfig, 25 | webpackMiddleware: { 26 | noInfo: true 27 | }, 28 | singleRun: true 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { Component, PluginFunction, ComponentOptions, VNode, CreateElement } from 'vue' 2 | 3 | declare module 'vue/types/vue' { 4 | export interface VueConstructor { 5 | createAPI: (Component: Component, events?: string[], single?: boolean) => Api 6 | } 7 | } 8 | 9 | export interface ApiOption { 10 | componentPrefix?: string 11 | apiPrefix?: string 12 | } 13 | 14 | export interface renderFunction { 15 | (createElement: CreateElement): VNode 16 | } 17 | 18 | export interface createFunction { 19 | (options: object, renderFn: renderFunction, single?: boolean):V 20 | (options: object, renderFn?: renderFunction):V 21 | (options: object, single?: renderFunction):V 22 | } 23 | 24 | export interface Api { 25 | before: (config: object,renderFn: renderFunction, single: boolean) => void, 26 | create: createFunction 27 | } 28 | 29 | export interface instantiateComponent { 30 | (Vue: Vue, Component: Component, data: object, renderFn: renderFunction, options: ComponentOptions): Component 31 | } 32 | 33 | export interface CreateAPI { 34 | batchDestroy: (filter?: (instances: Component[]) => Component[]) => void 35 | install: PluginFunction 36 | instantiateComponent: instantiateComponent 37 | version: string 38 | } 39 | 40 | declare const CreateAPI: CreateAPI 41 | export default CreateAPI 42 | 43 | // todo 扩展Component.$create方法 -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "es5", 8 | "dom" 9 | ], 10 | "strict": true, 11 | "noEmit": true 12 | }, 13 | "include": [ 14 | "*.d.ts" 15 | ] 16 | } --------------------------------------------------------------------------------