前言
年前入职新公司,公司要求搭建一个UI组件库,本专栏详细记录搭建思路和步骤,最后有项目github地址。
一、安装Vue CLI3
- 首先你要卸载Vue CLI2,用命令
npm uninstall vue-cli -g
卸载; - 输入命令
cnpm install -g @vue/cli
安装Vue CLI3; - 安装完成后,输入命令
vue --version
,查看Vue CLI的版本号是否是3.0以上,是则表明安装成功。
二、利用Vue CLI3 搭建Vue项目工程
- 1、在文件夹地址中输入cmd,打开命令行工具。
-
2、在命令行工具中输入
vue create foxit-ui
,foxit-ui
为项目名称。 -
3、按上下键选择Manually select features(手动选择功能)项,default (babel, eslint)(默认安装)。
- 4、按上下键和空格键选择要安装的功能。
- 5、选择要使用的Vue版本,选择Vue 2.0版本。
- 6、选择使用history 模式的路由器,输入Y,回车进入下一步。
- 7、选择css预编译语言,这里选择Less。
- 8、选择In dedicated config files,将Babel、ESLint等配置独立在package.json文件外。
- 9、是否保存安装配置,输入y,回车进入下一步。
- 10、开始下载Vue项目工程。如果下载很慢,是网络问题,可以让电脑连你的手机网络。
- 11、若出现如下图所示则表明已经下载成功。
- 12、分别执行
cd foxit-ui
和npm run serve
启动项目,执行成功后,在浏览器打开http://localhost:8080/,会出现如下图所示内容。
三、将Vue项目工程改造成Vue组件库工程
1、组件 demo 开发区的改造
组件 demo 将在Vue项目工程中 src 文件夹下开发,所以把文件名改成 examples,比较形象。
因为 src 文件夹中的 main.js 文件是原先Vue工程入口文件。src 文件夹名字被改动后,要重新配置一下入口文件地址。
使用Vue ClI3 搭建的Vue工程,是在工程根目录下创建一个 vue.config.js 来修改 webpack 的配置。
module.exports = {
pages: {
index: {
entry: 'examples/main.js'
}
}
}
重新执行命令 npm run serve
,会发现出现如图所示报错。这是因为 @
这个路径别名是代表 src 文件夹名的,现在 src 文件夹名已经不存在自然会报错。
这里先不管这个错误,先把原先 src 文件夹中的文件和代码做个清理。把其中 assets 文件夹、components 文件夹都删掉,view 文件夹中的文件都删掉,重新创建一个 demo.vue 文件,内容如下
<template>
<div>
<h1>foxit-ui</h1>
</div>
</template>
修改一下路由,在router/index.js 文件中。
import Vue from 'vue'
import VueRouter from 'vue-router'
import demo from '../views/demo.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'demo',
component: demo
},
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
再重新执行命令 npm run serve
,在浏览器打开http://localhost:8080/,会出现如下图所示内容。
2、组件开发区搭建
搭建一个组件库,里面包含很多组件,为了减小项目编译打包后体积的目的,故这里要实现按需引入的功能。参考 element UI 是借用插件 babel-plugin-component 来实现按需引入。
借用插件 babel-plugin-component 来实现组件库按需引入的前提是组件库是多入口编译打包的。
在实现多入口编译打包时,要注意在Vue组件库工程中开发环境下是单入口编译的,在生产环境下才是多入口编译的。
故采用环境变量 NODE_ENV
来区分是开发环境还是生产环境,当 NODE_ENV
为 development
时是开发环境。
环境变量 NODE_ENV
可以通过 process.env.NODE_ENV
来获取。故在 vue.config.js 文件中创建两个常量,常量 DEV_CONFIG
代表开发环境的配置,常量 BUILD_CONFIG
代表生产环境的配置。
const DEV_CONFIG = {
pages: {
index: {
entry: 'examples/main.js'
}
}
};
const BUILD_CONFIG = {
};
module.exports = process.env.NODE_ENV === 'development' ? DEV_CONFIG : BUILD_CONFIG;
在根目录下创建一个 packages 文件夹用来做组件开发区。因为在 Vue CLI3 中是默认对 src 文件夹中的内容做编译,所以 webpack 是不对 packages 文件夹中的内容进行编译,要把 packages 文件夹中的内容加入编译。在 vue.config.js 文件添加如下配置。
const DEV_CONFIG = {
pages: {
index: {
entry: 'examples/main.js',
}
},
chainWebpack: config => {
config.module
.rule('js')
.include
.add('/packages')
.end()
.use('babel')
.loader('babel-loader')
.tap(options => {
return options
});
},
};
const BUILD_CONFIG = {
};
module.exports = process.env.NODE_ENV === 'development' ? DEV_CONFIG : BUILD_CONFIG;
以上是使用链式操作操作来修改 webpack 的配置。具体语法可以在webpack-chain 中查看。
3、实现多入口编译打包组件库
工程的入口文件地址是通用 webpack 中 entry
选项来配置的。entry
选项的值是一个对象,所谓的多入口配置就是在 entry
选项上添加多个键值对,其键名可以用来配置打包后文件所在位置,键值就是入口文件的系统绝对路径。例如在 packages 文件夹中,开发 testA 和 testB 两个组件,每个组件的入口文件为组件所在文件夹下的 index.js 文件,另外主入口为 packages/index.js 文件。
module.exports = {
entry: {
index: 'E:\\project\\03UI\\foxit-ui\\packages\\index.js',
testA: 'E:\\project\\03UI\\foxit-ui\\packages\\testA\\index.js',
testB: 'E:\\project\\03UI\\foxit-ui\\packages\\testB\\index.js'
}
}
在 Vue CLI3 中,要先在 chainWebpack 中使用 config.entryPoints.delete('app')
把 entry
中 app 这个键值对删除,然后用 configureWebpack 来添加 entry
。在 vue.config.js 文件添加如下配置。
const DEV_CONFIG = {
};
const BUILD_CONFIG = {
chainWebpack: config => {
config.module
.rule('js')
.include
.add('/packages')
.end()
.use('babel')
.loader('babel-loader')
.tap(options => {
return options
});
config.entryPoints.delete('app');
},
configureWebpack: {
entry: {
index: 'E:\\project\\03UI\\foxit-ui\\packages\\index.js',
testA: 'E:\\project\\03UI\\foxit-ui\\packages\\testA\\index.js',
testB: 'E:\\project\\03UI\\foxit-ui\\packages\\testB\\index.js'
},
output: {
filename: '[name]/index.js',
libraryTarget: 'commonjs2',
}
},
};
module.exports = process.env.NODE_ENV === 'development' ? DEV_CONFIG : BUILD_CONFIG;
这里要记住在生产环境下也要把 packages 文件夹中的内容加入编译。
执行命令 npm run build
打包结果如下
从上图可以看出,output.filename
的值 [name]/index.js
中的 [name]
就是 entry
选项中的键名。
至于 libraryTarget: 'commonjs2'
的意思是各个入口文件打包后的值将会赋值给 module.exports
,也意味打包出来的文件将用于 CommonJS 环境 。
但是以上对入口文件的配置是写死,要利用 Nodejs 实现自动化配置。
4、实现自动化多入口文件的配置
通过上面的配置,可得知,只要用 Nodejs API 获取 packages 文件夹中每个组件的入口文件的绝对路径,在拼接成所需格式,在赋值给 entry
选项即可以实现自动化多入口文件的配置。
在 Nodejs 中有个 fs 模块可以获取文件信息,还有 path 模块可以用来处理路径。实现思路是用 fs 模块中的 API 获取 packages 文件夹下每个文件夹名称作为 entry
的键名,然后利用 path 模块中的 API 获取 packages 文件夹下每个文件夹中的 index.js 的系统绝对路径作为 entry
的键值。
以下是 fs 模块中要使用到的 API :
-
fs.readdirSync(path)
可以异步获取目录下所有文件名称的数组对象,其参数path
为目录所在的绝对路径。 -
fs.statSync(path)
可以异步返回一个文件的stat数组对象,其参数path
为文件所在的绝对路径。
以下是 path 模块中要使用到的 API:
-
path.resolve([...paths])
相当于在当前目录从左到右执行其 cd 每个参数的操作,最后返回当前目录的路径。例如:path.resolve('/foo/bar','baz')
相当于:cd /foo/bar //此时当前路径为 /foo/bar cd baz //此时路径为 /foo/bar/baz
-
path.join([...paths])
使用特定于系统的分隔符作为分隔符将所有给定的 path 段连接在一起,并且对结果路径进行规范化。
先贴出实现代码,在逐步分析。
const fs = require('fs');
const path = require('path');
function resolve(dir) {
return path.resolve(__dirname, dir);
}
const join = path.join;
function getEntries(path) {
let files = fs.readdirSync(resolve(path));
const entries = files.reduce((ret, item) => {
const itemPath = join(path, item);
const isDir = fs.statSync(itemPath).isDirectory();
if (isDir) {
ret[item] = resolve(join(itemPath, 'index.js'));
} else {
const [name] = item.split('.');
ret[name] = resolve(itemPath);
}
return ret;
}, {})
return entries;
}
先执行 const fs = require('fs'); const path = require('path');
引入 Nodejs 的 fs 模块 和 path 模块。
在 resolve
函数中 __dirname
在 Nodejs 中代表使用 __dirname
的 js 文件的系统绝对路径,例如在案例中 __dirname
的值为 E:\project\03UI\foxit-ui
,那么参数 dir
可以是文件名也可以是文件地址,例如:resolve('packages')
时,返回值是 E:\project\03UI\foxit-ui\packages
。
在 getEntries
函数中,当参数 path
为 packages
时,执行 fs.readdirSync(resolve(path))
返回 ['index.js', 'testA', 'testB']
赋值给变量 files
,再和工程中的 packages 文件夹目录结构一对比,是不是很清晰地了解 fs.readdirSync
的用处。
getEntries
函数的作用是返回一个 entry
选择的值。其值格式如下所示,
entry: {
index: 'E:\\project\\03UI\\foxit-ui\\packages\\index.js',
testA: 'E:\\project\\03UI\\foxit-ui\\packages\\testA\\index.js',
testB: 'E:\\project\\03UI\\foxit-ui\\packages\\testB\\index.js'
}
这里利用数组 reduce
方法来拼接出一个 entry
。reduce
的第一参数接收一个函数作为回调函数,再其中从左到右 处理files
数组中的每个值,处理结果通过回调函数的参数 ret
返回,ret
的初始值通过 reduce
的第二参数传入为 {}
。
执行 const itemPath = join(path, item)
先拼接一下返回 packages 文件夹目录下一级文件的地址,例如:packages\testA
、packages\index.js
。
假设 itemPath
的值是 packages\index.js
,已经获取到入口文件的相对地址,执行 resolve('packages\index.js')
处理一下,就可以得到其系统绝对地址。
假设 itemPath
的值是 packages\testA
,还要用 join('packages\testA', 'index.js')
拼接一下地址得到 packages\testA\index.js
,再执行 resolve('packages\testA\index.js')
处理得到 testA 组件的入口文件的系统绝对地址。
那么只要判断是 itemPath
路径所在资源是文件夹还是文件,就可以区分这两种场景分别做处理。
可以通过执行 fs.statSync(itemPath)
返回一个资源信息 stats
对象,且这个对象有个 isDirectory
方法可以判断该资源是文件夹还是文件。若该文件是文件夹返回 true
赋值给变量 isDir
。
当 isDir
为 true
时,执行 resolve(join(itemPath, 'index.js'))
得到入口文件的系统绝对路径赋值给 ret[item]
。
当 isDir
为 false
时,执行 resolve(itemPath)
得到入口文件的系统绝对路径后,还要对赋值给 ret
键名不能直接使用 item
,要执行 const [name] = item.split('.')
,这里使用了 ES6 的解构赋值,再赋值给 ret[name]
。
最后把 reduce
方法的返回值 ret
赋值 entries
并返回出去。
例如执行 getEntries('packages')
可以得到如下对象,可作为 entry
选项的值。
{
index: 'E:\\project\\03UI\\foxit-ui\\packages\\index.js',
testA: 'E:\\project\\03UI\\foxit-ui\\packages\\testA\\index.js',
testB: 'E:\\project\\03UI\\foxit-ui\\packages\\testB\\index.js'
}
那么在 vue.config.js 文件中改造一下配置:
const fs = require('fs');
const path = require('path');
function resolve(dir) {
return path.resolve(__dirname, dir);
}
const join = path.join
function getEntries(path) {
let files = fs.readdirSync(resolve(path));
const entries = files.reduce((ret, item) => {
const itemPath = join(path, item);
const isDir = fs.statSync(itemPath).isDirectory();
if (isDir) {
ret[item] = resolve(join(itemPath, 'index.js'));
} else {
const [name] = item.split('.');
ret[name] = resolve(itemPath);
}
return ret;
}, {})
return entries;
}
const DEV_CONFIG = {
};
const BUILD_CONFIG = {
chainWebpack: config => {
config.module
.rule('js')
.include
.add('/packages')
.end()
.use('babel')
.loader('babel-loader')
.tap(options => {
return options
});
config.entryPoints.delete('app');
},
configureWebpack: {
entry: {
...getEntries('packages')
},
output: {
filename: '[name]/index.js',
libraryTarget: 'commonjs2',
}
},
};
module.exports = process.env.NODE_ENV === 'development' ? DEV_CONFIG : BUILD_CONFIG;
执行命令 npm run build
打包结果如下
和之前打包结果一致,说明多入口文件的自动化配置实现成功。
5、配置组件库编译打包生成资源的存放位置
这里将组件库编译打包后放在 lib 文件夹中,在 vue.config.js 文件添加如下配置:
const BUILD_CONFIG = {
outputDir: 'lib',
}
module.exports = process.env.NODE_ENV === 'development' ? DEV_CONFIG : BUILD_CONFIG;
6、配置组件样式编译打包生成资源的存放位置
这里将组件库编译打包的样式文件放在 lib/style 文件夹中,在 vue.config.js 文件添加如下配置:
const BUILD_CONFIG = {
css: {
extract: {
filename: 'style/[name].css'
}
},
}
module.exports = process.env.NODE_ENV === 'development' ? DEV_CONFIG : BUILD_CONFIG;
7、关闭source map
关闭source map有两个好处
- 减少编译打包的时间。
- 避免在生产环境中用F12开发者工具在Sources中看到源码。
在 vue.config.js 文件添加如下配置
const BUILD_CONFIG = {
productionSourceMap: false,
}
module.exports = process.env.NODE_ENV === 'development' ? DEV_CONFIG : BUILD_CONFIG;
8、删除Vue CLI3原先编译打包的一些无用功能
- 删除splitChunks,因为每个组件是独立打包,不需要抽离每个组件的公共js出来。
- 删除copy,不要复制public文件夹内容到lib文件夹中。
- 删除html,只打包组件,不生成html页面。
- 删除preload以及prefetch,因为不生成html页面,所以这两个也没用。
- 删除hmr,删除热更新。
在 vue.config.js 文件添加如下配置
const BUILD_CONFIG = {
chainWebpack: config => {
config.optimization.delete('splitChunks');
config.plugins.delete('copy');
config.plugins.delete('html');
config.plugins.delete('preload');
config.plugins.delete('prefetch');
config.plugins.delete('hmr');
},
}
四、如何开发一个Vue 组件
上面已经简单的搭起了一个 Vue 组件库的发布框架。下面来搭建一个 Vue 组件库的开发框架。
首先要开发一个 Vue 组件,先回想一下是如何使用一个 Vue 组件,例如 element-ui 如何使用。
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
在 Vue 工程中一般是在 main.js 中添加上述代码来使用 element-ui,在其中 Vue.use(ElementUI)
是一个关键。
来看一下 Vue 官方文档对 Vue.use
的定义
其实 Vue 组件也是一个插件,在上面提到过插件必须提供一个 install 方法。在使用 Vue.use
注册插件时,会调用 install 方法。
再来看一下 Vue 官方文档对 install 方法的定义
那么在 install 方法中,使用 Vue.component
把所开发的组件注册成一个全局组件。这样就可以通过 Vue.use
来使用一个组件。
还要一个问题 install 方法要定义在哪里。这里直接给出答案,定义在组件的入口文件中。至于原因是跟 webpack 编译打包有关系,这里就不展开描述了。
在 packages 文件夹下创建一个 testA 文件夹,在里面专门写 testA 这个组件,在其中创建一个 index.js 文件作为 testA 组件的入口文件,在其中添加如下所示代码:
import test from './src/index.vue';
test.install = function (Vue) {
Vue.component(test.name, test);
};
export default test;
Vue.component
的第一个参数是组件的名字,第二个参数是组件的选项对象。从上述代码中可以看到组件的选项对象 test
是从 ./src/index.vue
import 出来的。
来看一下 testA/src/index.vue 文件的内容:
<template>
<div>测试按需引入A</div>
</template>
<script>
export default {
name: "testA",
data() {
return {};
},
};
</script>
对于上述的内容是不是非常熟悉,就是平时开发 Vue 页面所写的代码。至于上面所提到的组件的选项对象 test
就是 export default { }
中的内容。
执行 Vue.component(test.name, test)
其中 test.name
为 testA,注册了一个名为 testA 的全局组件。
因为一个组件库中有很多个组件,所以还需要一个总主入口文件,在 packages 文件夹下创建一个 index.js 文件,在其中添加以下代码:
import testA from './testA'
export default {
install(Vue) {
Vue.use(testA);
},
}
到这里已经算开发好了一个 testA 的 Vue 组件。那么要怎么调试呢?下面来搭建组件的调试环境,也可以称为组件的开发环境。
在前面提到过把原先 src 文件夹名称改为 examples 作为组件的调试区。那么可以在 examples 文件夹中 main.js 文件中引入所开发的组件。实现代码如下所示:
import test from '@/index'
Vue.use(test);
其中 @
是 packages 文件夹路径的别名,是在 vue.config.js 文件中这样配置的。
const DEV_CONFIG = {
configureWebpack: {
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'@': resolve('packages'),
}
}
}
}
module.exports = process.env.NODE_ENV === 'development' ? DEV_CONFIG : BUILD_CONFIG;
在 demo.vue 中如下所示使用 testA 组件。
<template>
<div style="text-align:center">
<img src="../assets/logo.png">
<testA></testA>
</div>
</template>
<script>
export default {
name: "demo",
data() {
return {
}
},
};
</script>
执行命令 npm run serve
,在浏览器打开http://localhost:8080/,会出现如下图所示内容。
这样一个组件库的发布和开发环境就搭建完成了。下面介绍如何把一个组件库发布到 npm 上。
五、如何把一个组件库发布到 npm 上
1、配置组件库名称
在 package.json 文件做如下配置
{
"name": "foxit-ui"
}
要注意这个组件库名称要在 npm 上是独一无二,不能重复,否则发布时会提示你无权限修改此库。
2、配值组件库版本号
在 package.json 文件做如下配置
{
"version": "0.0.1"
}
要注意这个组件库版本号不能重复,每次发布时都要手动更新一下版本号,否则发布时会提示不能在以前发布的版本上发布。
3、将组件库设置为公开
在 package.json 文件做如下配置
{
"private": false
}
只有把组件库设置为公开,才能在npm上发布。
4、配置关键词、描述、作者
在 package.json 文件做如下配置
{
"description": "基于element ui 二次封装的组件库",
"keywords": [
"element",
"vue",
"foxit-ui"
],
"author": "foxit"
}
5、配置主入口文件地址
在 package.json 文件做如下配置
{
"main": "lib/index/index.js"
}
主入口文件就是组件库编译打包生成的主入口文件的所在相对地址,在用 import 引入组件时,其实是在引入这个主入口文件。
6、设置忽略文件,减少依赖包大小
一个组件库只有编译后的 lib 文件夹、package.json 文件、README.md 文件才是需要被发布的。其他都不需要, 需要在根目录下创建一个 .npmignore 文件中把没必要发布的资源忽略掉,减少依赖包大小。
examples/
packages/
public/
node_modules/
vue.config.js
babel.config.js
output.js
postcss.config.js
.gitignore
.browserslistrc
7、编辑使用文档
在README.md,要写清楚,怎么安装,怎么引入使用,有哪些参数,哪些方法,哪些事件等,这是非常关键的。
8、登录npm
首先需要到 npm 上注册一个账号,注册过程略。
如果配置了淘宝镜像,先设置回 npm 镜像:
执行命令npm config set registry http://registry.npmjs.org
执行命令npm login
按步骤输入用户名、密码、邮箱,每个步骤回车进入下一步。
9、发布到npm
执行命令 npm publish
,如果成功如下图
10、将组件库同步到淘宝镜像
打开淘宝 NPM 镜像,将组件名 foxit-ui 输入进行搜索,点击下图所示进行同步
六、如何实现按需引入组件
为了演示按需引入组件,在组件库中再开发一个 testB 组件,并发布到 npm 上。
在上面提到过利用 babel-plugin-component 实现按需引入组件,那么先执行命令 npm install babel-plugin-import --save-dev
安装 babel-plugin-import 插件。
执行命令 cnpm install foxit-ui --save
安装 foxit-ui 组件库。
在根目录创建 .babelrc.js 文件,假如根目录有 babel.config.js 文件,可以直接在里面配置,配置内容如下
module.exports = {
presets: [
'@vue/app'
],
"plugins": [
[
"import",
{
"libraryName": "foxit-ui",//组件库名称
"camel2DashComponentName": false,//关闭驼峰自动转链式
"camel2UnderlineComponentName": false//关闭蛇形自动转链式
}
],
]
}
在 main.js 文件中添加如下代码
import {testA, testB} from 'foxit-ui';
Vue.use(testA);
Vue.use(testB);
在项目中这样使用
<template>
<div>
<testA></testA>
<testB></testB>
</div>
</template>
<script>
export default {
data() {
return {
}
},
}
</script>
浏览器展示
那么为什么是 import {testA, testB}
而不是 import {testC, testD}
,如果是import {testC ,testD}
,会发生如下报错
import {testA, testB} from 'foxit-ui';
相当
import testA from "foxit-ui/lib/testA";
import testB from "foxit-ui/lib/testB";
import {testC, testD} from 'foxit-ui';
相当
import testC from "foxit-ui/lib/testC";
import testB from "foxit-ui/lib/testD";
foxit-ui/lib中没有testC、testD这个文件,所以就报以上错误。
那为什么是 import {testA , testB}
,还记得在多入口文件页面打包配置中配置 entry
时,entry
选项的值的键名是每个组件所在的文件夹名,而 output.filename
是 [name]/index.js
,每个组件编译打包生成的资源存放的文件夹名称是原来每个组件所在文件夹名称。
这样是不是很清楚,为什么要import {testA , testB}
,是因为 testA 和 testB 两个组件打包生成的资源存放的文件夹名分别是 testA 和 testB。
另外为什么 output.filename
是 [name]/index.js
,而不是 [name]/main.js
或 [name]/out.js
。
这是因为import testA from "foxit-ui/lib/testA"
相当 import testA from "foxit-ui/lib/testA/index.js"
。
所以文档中写如何按需引入时,一定要写清楚引入方式。
七、如何实现按需引入组件的样式
上面实现按需引入组件,那么下面来配置实现在按需引入组件时也自动引入对应的样式,这样就不用去引入全部样式 import 'foxit-ui/lib/style/index.css'
。
在根目录下 .babelrc.js 文件 或 babel.config.js 文件中添加如下配置。
module.exports = {
"presets": ["@vue/app"],
"plugins": [
[
"import",
{
"libraryName": "foxit-ui",//组件库名称
"camel2DashComponentName": false,//关闭驼峰自动转链式
"camel2UnderlineComponentName": false,//关闭蛇形自动转链式
"style": (name) => {
const cssName = name.split('/')[2];
return `foxit-ui/lib/style/${cssName}.css`
}
}
],
]
}
当 style
选项为函数时,其函数返回值是组件样式文件的路径。其参数 name
是组件所在依赖包的相对路径,例如引入 testA 组件时,name
的值为 foxit-ui/lib/testB
。
在上面配置组件样式编译打包生成资源的存放位置中,是存放到 lib/style 文件夹中,其样式文件名和组件名一致,如下图所示。
故先执行 const cssName = name.split('/')[2]
获取当前引入组件的名称赋值给 cssName
,然后拼接地址 foxit-ui/lib/style/${cssName}.css
并返回。重新启动工程,最终页面展示如下所示。
后记
项目地址:foxit-ui
后续专栏会以实际组件开发,向大家介绍开发一个公共组件的具体思路。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!