目录
当你开发Vue 3应用程序时,有时你可能会需要将自己创建的组件发布到npm上,以便其他开发者可以轻松地在他们的项目中使用这些组件。本文将指导你如何将Vue 3组件发布到npm。
首先,确保你已经创建了你的Vue 3组件,并且它在本地正常工作。你可以使用Vue CLI或手动创建组件,然后使用Vue 3的组件系统进行开发。
npm init vite@latest
提示我们要安装create-vite@4.1.0
得依赖,选择y
起一个组件名字,然后我们选择vue
这里我选择的是javascript
,然后回车
安装完成
因为我们需要了element-ui组件库,所以我们要手动安装一下依赖
npm install element-plus --save
首先,我们要在src\components目录下,创建一个femessage文件夹,
在在femessage文件夹下创建el-form-renderer.vue 文件,(自定义名字)
- <template>
- <div>
- <el-form ref="myelForm" v-bind="$attrs" :model="value" class="el-form-renderer">
- <template v-for="item in innerContent" :key="item.id">
- <slot :name="`id:${item.id}`" />
- <slot :name="`$id:${item.id}`" />
-
- <component
- :is="item.type === GROUP ? RenderFormGroup : RenderFormItem"
- :ref="
- (el) => {
- customComponent[item.id] = el;
- }
- "
- :data="item"
- :value="value"
- :item-value="value[item.id]"
- :disabled="
- disabled ||
- (typeof item.disabled === 'function' ? item.disabled(value) : item.disabled)
- "
- :readonly="readonly || item.readonly"
- :options="options[item.id]"
- @updateValue="updateValue"
- />
- </template>
- <slot />
- </el-form>
- </div>
- </template>
-
- <script setup>
- import RenderFormGroup from "./components/render-form-group.vue";
- import RenderFormItem from "./components/render-form-item.vue";
- import {
- reactive,
- computed,
- ref,
- watch,
- onMounted,
- nextTick,
- provide,
- getCurrentInstance,
- } from "vue";
- import transformContent from "./util/transform-content";
- import _set from "lodash.set";
- import _isequal from "lodash.isequal";
- import _clonedeep from "lodash.clonedeep";
- import {
- collect,
- mergeValue,
- transformOutputValue,
- transformInputValue,
- correctValue,
- } from "./util/utils";
- let GROUP = "group";
- /**
- * inputFormat 让整个输入机制复杂了很多。value 有以下输入路径:
- * 1. 传入的 form => inputFormat 处理
- * 2. updateForm => inputFormat 处理
- * 3. 但 content 中的 default 没法经过 inputFormat 处理,因为 inputFormat 要接受整个 value 作为参数
- * 4. 组件内部更新 value,不需要走 inputFormat
- */
- let value = reactive({}); // 表单数据对象
- let options = reactive({});
- let initValue = reactive({});
- let myelForm = ref();
- let methods = {};
- const customComponent = ref([]);
- let emit = defineEmits(["update:FormData"]);
- // 注入 element ui form 方法
- /**
- * 与 element 相同,在 mounted 阶段存储 initValue
- * @see https://github.com/ElemeFE/element/blob/6ec5f8e900ff698cf30e9479d692784af836a108/packages/form/src/form-item.vue#L304
- */
- onMounted(async () => {
- initValue = _clonedeep(value);
- await nextTick();
- // 检查 myelForm 是否已经初始化
- if (myelForm && myelForm.value) {
- Object.keys(myelForm.value).forEach((item) => {
- // 检查属性是否存在于 methods 对象中
- if (myelForm.value[item] && !(item in methods)) {
- methods[item] = myelForm.value[item];
- }
- });
- }
- /**
- * 有些组件会 created 阶段更新初始值为合法值,这会触发 validate。目前已知的情况有:
- * - el-select 开启 multiple 时,会更新初始值 undefined 为 []
- * @hack
- */
- methods.clearValidate();
- });
-
- let props = defineProps({
- //表单项
- content: {
- type: Array,
- required: true,
- },
- // 禁用
- disabled: {
- type: [Boolean, Function],
- default: false,
- },
- //只读
- readonly: {
- type: Boolean,
- default: false,
- },
- /**
- * v-model 的值。传入后会优先使用
- */
- FormData: {
- type: Object,
- default: undefined,
- },
- });
- //兼容处理
- let innerContent = computed(() => transformContent(props.content));
- // 初始化默认值
- let setValueFromModel = () => {
- if (innerContent.length) return;
- /**
- * 没使用 v-model 时才从 default 采集数据
- * default 值没法考虑 inputFormat
- * 参考 value-format.md 的案例。那种情况下,default 该传什么?
- */
- let newValue = props.FormData
- ? transformInputValue(props.FormData, innerContent.value)
- : collect(innerContent.value, "default");
- correctValue(newValue, innerContent.value);
- if (!_isequal(value, newValue)) value = Object.assign(value, newValue);
- };
- // v-model初始化默认数据
- watch(
- () => props.FormData,
- (newForm) => {
- if (!newForm) return;
- setValueFromModel();
- },
- { immediate: true, deep: true }
- );
- // 初始化默认数据
- watch(
- innerContent,
- (newContent) => {
- try {
- if (!newContent) return;
-
- // 如果 content 没有变动 remote 的部分,这里需要保留之前 remote 注入的 options
- Object.assign(options, collect(newContent, "options"));
- setValueFromModel();
- } catch (error) {
- console.log(error);
- }
- },
- { immediate: true }
- );
-
- // v-model 传递值
- watch(value, (newValue, oldValue) => {
- try {
- if (!newValue) return;
- if (props.FormData) {
- let data = Object.assign(
- props.FormData,
- transformOutputValue(newValue, innerContent)
- );
- emit("update:FormData", data);
- }
- } catch (error) {
- console.log(error, "-----");
- }
- // deep: true, // updateValue 是全量更新,所以不用
- });
-
- /**
- * 更新表单数据
- * @param {String} options.id 表单ID
- * @param {All} options.value 表单数据
- */
- let updateValue = ({ id, value: v }) => {
- value[id] = v;
- };
- /**
- * 重置表单为初始值
- *
- * @public
- */
- let resetFields = async () => {
- /**
- * 之所以不用 el-form 的 resetFields 机制,有以下原因:
- * - el-form 的 resetFields 无视 el-form-renderer 的自定义组件
- * - el-form 的 resetFields 不会触发 input & change 事件,无法监听
- * - bug1: https://github.com/FEMessage/el-data-table/issues/176#issuecomment-587280825
- * - bug2:
- * 0. 建议先在监听器 watch.value 里 console.log(v.name, oldV.name)
- * 1. 打开 basic 示例
- * 2. 在 label 为 name 的输入框里输入 1,此时 log:'1' ''
- * 3. 点击 reset 按钮,此时 log 两条数据: '1' '1', '' ''
- * 4. 因为 _isequal(v, oldV),所以没有触发 v-model 更新
- */
- value = _clonedeep(initValue);
- await nextTick();
- methods.clearValidate();
- };
- /**
- * 当 strict 为 true 时,只返回设置的表单项的值, 过滤掉冗余字段, 更多请看 update-form 示例
- * @param {{strict: Boolean}} 默认 false
- * @return {object} key is item's id, value is item's value
- * @public
- */
- let getFormValue = ({ strict = false } = {}) => {
- return transformOutputValue(value, innerContent, { strict });
- };
- /**
- * update form values
- * @param {object} newValue - key is item's id, value is the new value
- * @public
- */
- let updateForm = (newValue) => {
- newValue = transformInputValue(newValue, innerContent);
- mergeValue(value, newValue, innerContent);
- };
- /**
- * update select options
- * @param {string} id
- * @param {array} options
- * @public
- */
- let setOptions = (id, O) => {
- _set(options, id, O);
- options = Object.assign(options); // 设置之前不存在的 options 时需要重新设置响应式更新
- };
-
- /**
- * get custom component
- * @param {string} id
- * @public
- */
- const getComponentById = (id) => {
- let content = [];
- props.content.forEach((item) => {
- if (item.type === GROUP) {
- const items = item.items.map((formItem) => {
- formItem.groupId = item.id;
- return formItem;
- });
- content.push(...items);
- } else {
- content.push(item);
- }
- });
- const itemContent = content.find((item) => item.id === id);
- if (!itemContent) {
- return undefined;
- }
- if (!itemContent.groupId) {
- return customComponent.value[id].customComponent;
- } else {
- const componentRef = customComponent.value[itemContent.groupId].customComponent;
- return componentRef[`formItem-${id}`].customComponent;
- }
- };
- provide("methods", methods);
- provide("updateForm", updateForm);
- provide("setOptions", setOptions);
- defineExpose({
- updateValue,
- resetFields,
- getFormValue,
- updateForm,
- setOptions,
- methods,
- getComponentById,
- });
- </script>
- <script>
- export default {
- name: "ElFormRenderer",
- };
- </script>
src
根目录中创建index.js
文件,代码如下:
- import elFormRenderer from "./components/femessage/el-form-renderer.vue"; // 引入封装好的组件
- export { elFormRenderer }; //实现按需引入*
- const coms = [elFormRenderer]; // 将来如果有其它组件,都可以写到这个数组里
-
- const components = [elFormRenderer];
- const install = function (App, options) {
- components.forEach((component) => {
- App.component(component.name, component);
- });
- };
- export default { install }; // 批量的引入*
使用vite构建
编辑vite.config.js
文件,新增build
属性 vite中文文档https://cn.vitejs.dev/guide/build.html#library-mode
build
:这是一个包含构建选项的对象。
lib
:这是构建库的选项。
entry
:指定了库的入口文件,通常是一个 JavaScript 文件的路径。name
:定义了库的名称,这里是 "el-form-renderer-vue3"。fileName
:定义了输出文件的命名规则,使用了一个函数来生成不同格式的文件名。rollupOptions
:这是用于 Rollup 构建工具的选项,Rollup 通常用于打包 JavaScript 库。
external
:指定了需要排除的外部依赖项,这里只有 "vue"。
output
:定义了输出选项。
globals
:在 UMD 构建模式下,指定了外部依赖的全局变量名,这里将 "vue" 映射到 "Vue",以确保在使用库时可以访问到 Vue.js。- import { defineConfig } from "vite";
- import vue from "@vitejs/plugin-vue";
- import path from "path";
-
- // https://vitejs.dev/config/
- export default defineConfig({
- plugins: [vue()],
- build: {
- lib: {
- entry: path.resolve(__dirname, "src/index.js"),
- name: "el-form-renderer-vue3",
- fileName: (format) => `el-form-renderer-vue3.${format}.js`,
- },
- rollupOptions: {
- // 确保外部化处理那些你不想打包进库的依赖
- external: ["vue"],
- output: {
- // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
- globals: {
- vue: "Vue",
- },
- },
- },
- },
- });
修改package.json
文件
- {
- "name": "el-form-renderer-vue3",
- "version": "1.0.1",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "preview": "vite preview"
- },
- "files": [
- "dist"
- ],
- "main": "./dist/el-form-renderer-vue3.umd.js",
- "module": "./dist/el-form-renderer-vue3.es.js",
- "exports": {
- ".": {
- "import": "./dist/el-form-renderer-vue3.es.js",
- "require": "./dist/el-form-renderer-vue3.umd.js"
- }
- },
- "dependencies": {
- "@element-plus/icons-vue": "^2.1.0",
- "axios": "^1.5.1",
- "element-plus": "^2.3.14",
- "lodash.clonedeep": "^4.5.0",
- "lodash.frompairs": "^4.0.1",
- "lodash.get": "^4.4.2",
- "lodash.has": "^4.5.2",
- "lodash.includes": "^4.3.0",
- "lodash.isequal": "^4.5.0",
- "lodash.isplainobject": "^4.0.6",
- "lodash.kebabcase": "^4.1.1",
- "lodash.set": "^4.3.2",
- "lodash.topairs": "^4.3.0",
- "vue": "^3.3.4",
- "vue-router": "4"
- },
- "devDependencies": {
- "@vitejs/plugin-vue": "^4.2.3",
- "vite": "^4.4.5"
- }
- }
当我们都配置好以后,我们就要打包了,这是我们要上传得文件
打包,生成dist
文件
npm run build
注册npm账号 官网地址
想要发布到npm仓库,就必须要有一个账号,先去npm官网注册一个账号,注意记住用户名、密码和邮箱,发布的时候可能会用到
有些小伙伴可能本地的npm镜像源采用的是淘宝镜像源或者其它的,如果想要发布npm包,我们得吧我们得npm源切换为官方得源,命令如下:
npm config set registry=https://registry.npmjs.org
发布前准备
在dist
文件生成package.json
文件,自定义组件名(唯一,重名报错重新起一个就行),版本号每次上传要高于前一次版本号
在dist
根目录中运行:
npm init -y
- {
- "name": "el-form-renderer-vue3",
- "version": "1.0.2",
- "description": "",
- "main": "el-form-renderer-vue3.es.js",
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
- },
- "keywords": [],
- "author": "",
- "license": "ISC"
- }
登录npm
用户
npm login
Username
用户名Password
密码Email
邮箱地址Enter one-time password
验证码执行发布命令
npm publish
已经上传成功
当我们要在项目中使用的时候就复制 npm i el-form-renderer-vue3
package.json
文件中就有了我们安装的组件
这个时候只要像element ui
那样引入就可以全局使用了,在main.js
中引入
在我们要用到得.vue
中使用
如果你还没有npm账户,你需要先创建一个。前往npm官方网站注册一个账户。
注意:记住你填写的用户名、邮箱、密码,等下你在本地是需要用这些信息登录的。
在本地登录你刚刚注册的账号
如果是npm,最后还会给你的邮箱发个验证码,填上之后再回车,才算真正登录成功。
执行 npm login
,输入用户名、密码以及你注册时的邮箱。
使用以下命令在终端中登录到你的npm账户:
npm login
这将提示你输入你的npm用户名、密码和邮箱地址。
因为我们要发布到官方源上面,所以要确保源地址为官方地址 http://registry.npmjs.org
或 https://registry.npmjs.com
,
可以通过 npm config get registry
命令查看当前 registry 源。
推荐使用nrm
管理本地npm源。
全局安装
nrm:
npm install -g nrm
查看npm源:
nrm ls
会把所有的源都列出来,其中带*的是当前使用的源
添加npm源:
nrm add xxx http://xxxnpm.cn/
xxx 是你给这个源起的名字,后面跟上源的URL
删除源:
nrm del xxx
xxx 是你给这个源起的名字
最后,运行以下命令来发布你的Vue 3组件到npm:
npm publish
创建一个Node.js项目时,package.json
文件是非常重要的,它包含了项目的配置信息和依赖项。下面是一个典型的package.json
文件及其各个字段的介绍:
name
: 项目的名称,应该是唯一的。version
: 项目的版本号,遵循语义化版本规则。description
: 项目的简要描述。main
: 指定项目的入口文件。scripts
: 自定义命令脚本,例如,你可以运行 npm start
启动应用。keywords
: 一组关键字,有助于其他人找到你的项目。author
: 作者的信息。license
: 项目的许可证,常见的包括 MIT、Apache-2.0、GPL-3.0 等。repository
: 指定项目的代码仓库信息,包括仓库类型和URL。bugs
: 定义问题跟踪系统的URL,以及可选的问题报告邮箱。homepage
: 项目的主页URL,通常是项目在代码托管平台上的页面。接下来是依赖项:
dependencies
: 生产依赖项,这些包在生产环境中需要。devDependencies
: 开发依赖项,这些包在开发和测试过程中需要,但不会包含在生产环境中。- {
- "name": "my-node-app",
- "version": "1.0.0",
- "description": "My Node.js Application",
- "main": "index.js",
- "scripts": {
- "start": "node index.js",
- "test": "mocha"
- },
- "keywords": ["node", "javascript"],
- "author": "Your Name",
- "license": "MIT",
-
- "repository": {
- "type": "git", // 仓库类型
- "url": "https://github.com/yourusername/my-node-app.git" // 仓库 URL
- },
-
- "bugs": {
- "url": "https://github.com/yourusername/my-node-app/issues", // 问题跟踪系统 URL
- "email": "youremail@example.com" // 可选的问题报告邮箱
- },
-
- "homepage": "https://github.com/yourusername/my-node-app", // 项目主页 URL
-
- "dependencies": {
- "express": "^4.17.1",
- "body-parser": "^1.19.0"
- },
- "devDependencies": {
- "mocha": "^8.0.1",
- "chai": "^4.2.0"
- }
- }
要发布一个npm 包,name 和 version 字段是必填的;
包名(name 字段)命名规则:
版本号(version 字段)则需要遵循 semver 规范。
填写好 package.json 字段后,接下来就是确定我们要发布的版本号,每次对包的更改都应该对应一个版本。
格式:MAJOR.MINOR.PATCH
,值非负整数,且禁止在数字前面补 0
MAJOR
:主版本号MINOR
:次版本号PATCH
::修订号当某个版本还不稳定的时候,还可能要先发布一个先行版本,具体可看 semver 规范。
当你开发面向浏览器的库时,你可能会将大部分时间花在该库的测试/演示页面上。在 Vite 中你可以使用 index.html
获得如丝般顺滑的开发体验。
当这个库要进行发布构建时,请使用 build.lib 配置项,以确保将那些你不想打包进库的依赖进行外部化处理,例如 vue
或 react
:
- // vite.config.js
- import { resolve } from 'path'
- import { defineConfig } from 'vite'
-
- export default defineConfig({
- build: {
- lib: {
- // 也可以是字典或多个入口点的数组
- entry: resolve(__dirname, 'lib/main.js'),
- name: 'MyLib',
- // the proper extensions will be added
- fileName: 'my-lib',
- },
- rollupOptions: {
- // 确保外部化处理那些你不想打包进库的依赖
- external: ['vue'],
- output: {
- // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
- globals: {
- vue: 'Vue',
- },
- },
- },
- },
- })
入口文件将包含可以由你的包的用户导入的导出:
- // lib/main.js
- import Foo from './Foo.vue'
- import Bar from './Bar.vue'
- export { Foo, Bar }
使用如上配置运行 vite build
时,将会使用一套面向库的 Rollup 预设,并且将为该库提供两种构建格式:es
和 umd
(可在 build.lib
中配置):
- $ vite build
- building for production...
- dist/my-lib.js 0.08 KiB / gzip: 0.07 KiB
- dist/my-lib.umd.cjs 0.30 KiB / gzip: 0.16 KiB
荐在你库的 package.json
中使用如下格式:
- {
- "name": "my-lib",
- "type": "module",
- "files": ["dist"],
- "main": "./dist/my-lib.umd.cjs",
- "module": "./dist/my-lib.js",
- "exports": {
- ".": {
- "import": "./dist/my-lib.js",
- "require": "./dist/my-lib.umd.cjs"
- }
- }
- }
或者,如果暴露了多个入口起点:
- {
- "name": "my-lib",
- "type": "module",
- "files": ["dist"],
- "main": "./dist/my-lib.cjs",
- "module": "./dist/my-lib.js",
- "exports": {
- ".": {
- "import": "./dist/my-lib.js",
- "require": "./dist/my-lib.cjs"
- },
- "./secondary": {
- "import": "./dist/secondary.js",
- "require": "./dist/secondary.cjs"
- }
- }
- }
注意
如果
package.json
不包含"type": "module"
,Vite 会生成不同的文件后缀名以兼容 Node.js。.js
会变为.mjs
而.cjs
会变为.js
。
环境变量
在库模式下,所有
import.meta.env.*
用法在构建生产时都会被静态替换。但是,process.env.*
的用法不会被替换,所以你的库的使用者可以动态地更改它。如果不想允许他们这样做,你可以使用define: { 'process.env.NODE_ENV': '"production"' }
例如静态替换它们。
{ entry: string | string[] | { [entryAlias: string]: string }, name?: string, formats?: ('es' | 'cjs' | 'umd' | 'iife')[], fileName?: string | ((format: ModuleFormat, entryName: string) => string) }
构建为库。
entry
是必需的,因为库不能使用 HTML 作为入口。name
则是暴露的全局变量,并且在 formats
包含 'umd'
或 'iife'
时是必需的。默认 formats
是 ['es', 'umd']
,如果使用了多个配置入口,则是 ['es', 'cjs']
。fileName
是输出的包文件名,默认 fileName
是 package.json
的 name
选项,同时,它还可以被定义为参数为 format
和 entryAlias
的函数。自定义底层的 Rollup 打包配置。这与从 Rollup 配置文件导出的选项相同,并将与 Vite 的内部 Rollup 选项合并。查看 Rollup 选项文档 获取更多细节。