Vue-cli 4.x 组件库开发遇到哪些事

Aditya2024-01-29前端Vue

Init

当用脚手架命令vue create project-component生成项目的时候,它的结构如下:

.
├── project-component
│   └── node_modules
│   ├── public
│   ├── src
│   ├── babel.config.js
│   ├── package.json
│   └── README.md

首先创建一个packages目录,用来存放组件

然后将src目录改为examples用作示例

.
├── project-component
│   └── examples
│   ├── node_modules
│   ├── packages
│   ├── public
│   ├── babel.config.js
│   ├── package.json
│   └── README.md

修改配置

启动项目的时候,默认入口文件是 src/main.js

将 src 目录改为 examples 之后,就需要重新配置入口文件

在根目录下创建一个 vue.config.js 文件

// vue.config.js 

module.exports = {
  // 将 examples 目录添加为新的页面
  pages: {
    index: {
      // page 的入口
      entry: 'examples/main.js',
      // 模板来源
      template: 'public/index.html',
      // 输出文件名
      filename: 'index.html'
    }
  }
}

Vue-cli 提供了构建库open in new window的命令,所以这里不需要再为packages目录配置webapck。

开发组件

packages目录存放组件,该目录下存放每个组件单独的开发目录,和一个 index.js 整合所有组件,并对外导出,每个组件都应该归类于单独的目录下,包含其组件源码目录 src,和 index.js 便于外部引用

需要注意的是,组件必须声明 name,这个 name 就是组件的标签

import DrReading from '@/views/home.vue'

DrReading.install = function (Vue) {
    Vue.component(DrReading.name, DrReading)
}

export default DrReading

单个组件导出后,在packages/index.js 统一实现全局注册

// 导入单个组件
import DrReading from './views/index'

// 以数组的结构保存组件,便于遍历
const components = [
    DrReading
]

// 定义 install 方法
const install = function (Vue) {
    // if (install.installed) return
    // install.installed = true
    // 遍历并注册全局组件
    components.map(component => {
        console.log(component)
        Vue.component(component.name, component)
    })
}

if (typeof window !== 'undefined' && window.Vue) {
    install(window.Vue)
}

export default {
    // 导出的对象必须具备一个 install 方法
    install,
    // 组件列表
    ...components
}

到这里组件就已经开发完毕

可以在 examples/main.js 中引入该组件

// examples/main.js
import DwReading from '../packages/index.js'
Vue.use(DwReading)

// App.vue
<dr-reading />

打包组件

vue-cli 提供了一个库文件打包命令open in new window

主要需要四个参数:

\1. target: 默认为构建应用,改为 lib 即可启用构建库模式

\2. name: 输出文件名

  1. dest: 输出目录,默认为 dist,这里我们改为 lib

\4. entry: 入口文件路径,默认为 src/App.vue,这里改为 packages/index.js

基于此,在 package.json 里的 scripts 添加一个 lib 命令

{
    "scripts": {
        "serve": "vue-cli-service serve",
        "build": "vue-cli-service build",
        "lint": "vue-cli-service lint",
        "lib": "vue-cli-service build --target lib --name dr-reading --dest lib packages/index.js",
        "release": "npm run lib && npm publish"
    },
    ...
    "files": [
        "lib"
    ],
    "main": "./lib/dr-reading.umd.min.js"
}

发布

package.json 添加组件信息,然后创建**.npmignore** 文件,设置忽略文件

该文件的语法和 .gitignore 的语法一样,设置发布到 npm 时忽略哪些目录或文件

.DS_Store
node_modules/
examples/
packages/
public/
vue.config.js
babel.config.js
*.map
*.html


# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

创建 .npmrc ,设置发布地址

registry=http://registry.npmjs.org // 或者私库

如果是发布到npm,需要通过npm login登录或者到通过 npm adduser创建账号,具体流程可参考官方文档。

最后执行:

npm publish

发布失败

我发布到私库的时候,报了以下问题

413 Payload Too Large - PUT http://xxx- request entity too large

查到的解决方案

此时找到verdaccio的config.yaml,

如: /root/.config/verdaccio/config.yaml

修改 max_body_size: 100mb

重启verdaccio 就可以解决

主要是平时开发时,有npm包需要在本地调试好了再发布。发一版测一版,或者把代码复制粘贴到项目文件夹里去调试,很不优雅。软链就变得极为有用了,特别是需要调试的npm包不止一个,且彼此之间需要联调。

使用

我想想在项目B里边用项目A的本地npm包。

先在对应npm包的文件创建一个全局的链接

cd ~/projects/projectA
npm link  

然后再想要使用该包的项目里使用这个软链

注意这里的packageName一定要对应你的npm包package.json里的name字段值。

cd ~/projects/projectB
npm link packageName   
npm unlink packageName // 使用npm包的项目的文件目录下解除特定的链接
npm unlink // 在npm包所在的文件目录下去除全局链接

Runtime Error integrating a component lib that uses @vue/composition-api: 'You must use this function within the "setup()" method'

The Actual Problem

在打包好组件库引入到项目中时,控制台报了一连串错误:

Runtime Error integrating a component lib that uses @vue/composition-api: 
'You must use this function within the "setup()" method'

当看到这个问题,我也是一脸懵逼,主项目里也引入了 @vue/composition-api ,也能正常使用,为什么到了这个组件引入就会报这么个错误。

尝试了各种方法,发现还是出组件库里的 @vue/composition-api ,我把组件库清理干净重新打个包引入,能正常运行,恢复到 @vue/composition-api 引入就异常。

确定了问题所在就开始寻找解决方法。

Solution

经过在stackoverflow上有人遇到跟我一样的问题:

Runtime Error integrating a component lib that uses @vue/composition-api: 'You must use this functioopen in new window

有大佬给出了问题产生原因及解决方法。

The child-component-lib was bundling their own versions of the npm packages @vue/composition-api and vuex-composition-helpers. This had the following effect: When I was running the parent-app there were actually two instances of those libraries and the vue component from the child-component-lib was accessing the wrong object that had not been properly initialized.

这是由于子组件库捆绑了他们自己版本的 npm 包 @vue/composition-api 和 vuex-composition-helpers。这产生了以下影响:当我运行父应用程序时,实际上有两个库实例,而子组件库中的vue组件正在访问没有正确初始化的错误对象。

解决方案是通过以下方法防止在子组件库中捆绑这些库:

  1. devDependenciespeerDependencies中引入它们;

  2. 并且在构建的时候使webpack不要打包它们。

package.json
"dependencies": {
    ...
},
"devDependencies": {"@vue/composition-api": "^1.0.0-beta.19","vuex-composition-helpers": "^1.0.21",
    ...
},
"peerDependencies": {"@vue/composition-api": "^1.0.0-beta.19","vuex-composition-helpers": "^1.0.21"
},
vue.config.js
configureWebpack: {
    externals: {
        "@vue/composition-api": "@vue/composition-api",
        "vuex-composition-helpers": "vuex-composition-helpers"
    },
    ...
}

peerDependencies是什么东西,这里为什么需要它?

初试 peerDependencies

为了搞懂上述问题是怎么解决,特意去查了下peerDependencies是什么东西,在此记录一下。

example

假设现在有一个 helloWorld 工程,已经在其 package.json 的 dependencies 中声明了 packageA,有两个插件 plugin1 和 plugin2 他们也依赖 packageA,如果在插件中使用 dependencies 而不是 peerDependencies 来声明 packageA,那么 $ npm install 安装完 plugin1 和 plugin2 之后的依赖图是这样的:

.
├── helloWorld
│   └── node_modules
│       ├── packageA
│       ├── plugin1
│       │   └── nodule_modules
│       │       └── packageA
│       └── plugin2
│       │   └── nodule_modules
│       │       └── packageA

从上面的依赖图可以看出,helloWorld 本身已经安装了一次packageA,但是因为因为在 plugin1 和 plugin2 中的 dependencies 也声明了 packageA,所以最后 packageA 会被安装三次,有两次安装是冗余的。

peerDependency 就可以避免类似的核心依赖库被重复下载的问题。

如果在 plugin1 和 plugin2 的 package.json 中使用 peerDependency 来声明核心依赖库,例如:

plugin1/package.json

{
    "peerDependencies": {
        "packageA": "1.0.1"
    }
}

plugin2/package.json

{
    "peerDependencies": {
        "packageA": "1.0.1"
    }
}

在主系统中声明一下 packageA:

helloWorld/package.json

{
    "dependencies": {
        "packageA": "1.0.1"
    }
}

此时在主系统中执行 $ npm install 生成的依赖图就是这样的:

.
├── helloWorld
│   └── node_modules
│       ├── packageA
│       ├── plugin1
│       └── plugin2

可以看到这时候生成的依赖图是扁平的,packageA 也只会被安装一次。

因此我们总结下在插件使用 dependencies 声明依赖库的特点:

  • 如果用户显式依赖了核心库,则可以忽略各插件的 peerDependency 声明;

  • 如果用户没有显式依赖核心库,则按照插件 peerDependencies 中声明的版本将库安装到项目根目录中;

  • 当用户依赖的版本、各插件依赖的版本之间不相互兼容,会报错让用户自行修复;

又双叒叕 Composition-API

在这个组件库正式上线之后,我寻思写个使用文档,并且做一个demo贴上,以供组内同学们参考一下。然鹅刚在一个 vue create demo 的新项目引入就出现了问题。

img

这个项目很干净,干净到只新增了两个依赖。

"dependencies": {
  "core-js": "^3.6.5",
  "vue": "^2.6.11"
},
"devDependencies": {
  "@vue/cli-plugin-babel": "~4.5.6",
  "@vue/cli-plugin-eslint": "~4.5.6",
  "@vue/cli-service": "~4.5.6",
  "babel-eslint": "^10.1.0",
  "eslint": "^6.7.2",
  "eslint-plugin-vue": "^6.2.2",
  "vue-template-compiler": "^2.6.11",
  "@vue/composition-api": "^1.6.2",
  "axios": "^0.26.1"
},

我下意识是因为 @vue/composition-api 版本不一致导致的,我就把所有的引入版本跟组件库保持一致,但还是引入异常。

经过控制变量法以及一系列匪夷所思的操作下,我终于找到了原因。

"^"

在package-lock里我查看到,vue下载的版本是2.7.x,原因是没锁死版本,"vue": "^2.6.11",会自动下载vue2大版本下最新的库,vue2.7.x内置composition-api,所以导致了该问题,离离原上谱。

webpack编译及其他问题

这里主要是由于webapck配置问题,出现的结果不如预期

丢失css样式

手动导入构建后的css文件,也可以构建时通过在 vue.config.js 中设置 css: { extract: false } 强制内联

// vue.config.js

module.export = {
    ...
    css: { extract: false },
}

图片没有转成base64

由于需要最后生成的组件库只是一个js文件的话,就需要保证库的“干净”,类似assetsstatic

里的文件都要转成base64引入。

vue-webpack模板的默认设置限制了转码的文件大小为10000B以下

// vue.config.js

module.export = {
    ...
    chainWebpack: (config) => {
        config.module
        .rule("images")
        .use("url-loader")
        .loader("url-loader")
        .options({
          limit: 1024 * 10,
          fallback: {
            loader: "file-loader",
            options: {
              name: "img/[name].[hash:8].[ext]",
              publicPath,
            },
          },
        })
        .end();
    }
}

vuex数据初始化失败

排查后发现没有引入vuex依赖,导致所有store中存储的数据全部为undfined

在构建 Web Components 组件open in new windowopen in new window时,入口点不是 main.js ,而是 entry-wc.js 文件,因此,要在 Web Components 组件的目标中使用 vuex ,你需要在 App.vue 中初始化存储 (store):

import store from './store'
// ...
export default {
  store,
  name: 'App',
  // ...
}
Last Updated 2024/12/27 11:36:49