ONE DUDE`S BLOG

/media/vuejslogo.webp

Общий модуль для нескольких vue js 3 приложений

14.03.2021
Создание общего модуля для vue js 3 приложений с использованием локального npm пакета, а также создания разных бандлов vue 3 для сборки.

Зачем?

На самом деле, если вы вдруг решили, что вам нужна локальная библиотека, подумайте еще раз. Скорее всего, она вам не нужна. На этом эту статью можно было бы и закончить.. но, увы, мир не совершенен. Зачастую, данные требования обусловлены желанием заказчика, не совсем корректным ТЗ, а также какими то другими специфическими обстоятельствами. Если это ваш случай, тогда добро пожаловать под кат.

Проблема

Представим ситуацию: имеем несколько проектов vue js, каждый из которых имеет общие зависимости (dumb ui компоненты, api вызовы, модели предметной области, утилиты и тд). Каким-то образом необходимо иметь доступ к общим компонентам vue js, а также раздельные скрипты и представления для сборки проектов.

Решение

Первым наиболее очевидным (и менее болезненным) является публикация приватного нпм пакета. Я не буду рассматривать этот метод детально, лишь этап сборки.

Локальный модуль. Данный подход в целом неплох, однако придется каждый раз перекомпилировать общий модуль, в случае активной разработки на начальном этапе это не совсем удобно, к тому же, в случае vue js, есть определенные проблемы с поддержкой типов во внешнем модулей.К тому же, в случае vue js, есть определенные проблемы с поддержкой типов во внешнем модулей (актуально на момент написания статьи).

Создание нескольких скриптов для сборки проектов. Данный метод позволяет использовать общие зависимоти с hot reload. К минусам можно отнести синхронизацию работы в больших командах. 1 репозиторий, жирные мерджи, конфликты и т.д. Для небольших же команд, минусов я не обнаружил.

Реализация через локальный пакет

Для начала разберем реализацию с локальным модулем. Наша структура будет выглядеть так (каждый из проектов может быть вынесет в отдельный git submodule).

-   src
    -   dist
    -   components
    -   index.ts
    ...
project1
-   src
    -   components
    ...
project2
-   src
    -   components
        ...

Для начала ицнииализируем нашу будущее библиотеку, для этого вводим привычное `vue create shared, устанавливаем необходимые конфиги.

Для примера создадим общий HelloWorld.vue компонент, он у нас будет полезным, выводить Hello world мигающими буквами.

<template>
    <h1 class="awesome-title">{{ msg }}</h1>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
    props: {
        msg: String
    }
});
</script>

<style scoped lang="scss">
.awesome-title {
    color: purple;
}
</style>

Внутри shared module будем использовать единый ентрипоинт index.ts.

export { default as HelloWorld } from './components/HelloWorld.vue';

Для поддержки автокомплита, а также для работы с typescript нам необходимо задекларировать типы для экспортируемых компонентов. Для этого создадим директорию types в нашем общем модуле. Создадим файл index.d.ts с объявлением типов. Получим следующую стуркутру общего модуля:

shared_module
    - src
    - dist
    - components
    - index.ts
    - types
      - index.d.ts
    ...
import { VueConstructor } from 'vue';

export const HelloWorld: VueConstructor;

Теперь осталось собрать наш пакет и импортировать как зависимость в других vue приложениях.

Для этого создадим скрипт в package.json. Также добавим сразу название файла для выходного бандла (в нашем случае shared.umd.min.js). Также добавим файлы которые будут входить в состав пакета. Отдельно объявим файл с типами.

 {
    "scripts": {
        "build:lib": "vue-cli-service build --target lib --name shared ./src/index.ts && npm pack && mv shared0.1.0.tgz ../shared.tgz"
    },
    "main": "./dist/shared.umd.min.js",
    "files": [
        "dist/*",
        "types/*"
    ],
    "types": "./types/index.d.ts",
}

Если у вас есть дополнительные файлы, которые бы вы хотели экспортировать, используйте ключ “exports”

{
    "exports": {
        "./src/styles/index.scss": "./dist/styles/index.scss"
    }
}

Для поддержки автокомплита, а также для работы с typescript нам необходимо задекларировать типы для экспортируемых компонентов. Для этого создадим директорию types в нашем общем модуле. Создадим файл index.d.ts с объявлением типов. Получим следующую стуркутру общего модуля:

shared_module
    - src
    - dist
    - components
    - index.ts
    - types
      - index.d.ts
    ...
import { VueConstructor } from 'vue';

export const HelloWorld: VueConstructor;

Теперь осталось собрать наш пакет и импортировать как зависимость в других vue приложениях.

Для этого создадим скрипт в package.json. Также добавим сразу название файла для выходного бандла (в нашем случае shared.umd.min.js). Также добавим файлы которые будут входить в состав пакета. Отдельно объявим файл с типами.

{
  "scripts": {
    "build:lib": "vue-cli-service build --target lib --name shared ./src/index.ts && npm pack && mv shared0.1.0.tgz ../shared.tgz"
  },
  "main": "./dist/shared.umd.min.js",
  "files": [
    "dist/*",
    "types/*"
  ],
 "types": "./types/index.d.ts",
}

Если у вас есть дополнительные файлы, которые бы вы хотели экспортировать, используйте ключ "exports"

{
  "exports": {
    "./src/styles/index.scss": "./dist/styles/index.scss"
  }
}

Корябаем npm run build:lib (или yarn build:lib). На выходе получаем структуру:

    shared
     - src
       - dist
       - components
       - index.ts
       ...
    project1
     - src
       - components
       ...
    project2
     - src
       - components
       ...
    shared.tgz (наша скомпилированая библиотека)

Отлично, теперь можно импортировать скопилированную библиотеку как зависимость в нашем 2 приложении vue js.

    shared
     - src
       - dist
       - components
       - index.ts
       ...
    project1 (*)
     - src
       - components
       ...
    project2
     - src
       - components
       ...
    shared.tgz (наша скомпилированая библиотека)

Для этого пишем перейдем в project1 (не забудем сначала создать его через vue create project1) и установим наш модуль npm i ../shared-0.1.0.tgz

В package.json должно появится

{
  "dependencies": {
    "core-js": "^3.6.5",
    "shared": "file:../shared-0.1.0.tgz",
    "vue": "^3.0.0",
    "vue-class-component": "^8.0.0-0"
  }
}

Теперь можно попробовать использовать наш компонент в project1, для этого в App.vue:

<template>
  <HelloWorld msg="Our shared component" />
</template>

<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { HelloWorld } from "shared";

 import 'shared/dist/shared.css';

@Options({
  components: {
    HelloWorld
  }
})
export default class App extends Vue {}
</script>

Запускаем проект и наслаждаемся.

Реализация через разные сборки

В данном подходе структура проекта будет выглядеть следующим образом:
    
project
    - src
     - shared
     - project1
      - view
      - components
      - main.ts
     - project2
      - view
      - components
      - main.ts
       ...
    - dist
    vue.config.js
    ...

В данном случае у нас будет лишь 1 node_modules, package.json и 2 2 dist. После создания проекта (привычным vue create) необходимо создать подпроекты в src. В каждом подпроекте есть main.ts, свои view, роуты и компоненты. Из магии остается только сборка проекта:

{
  "scripts": {
    "serve:project1": "vue-cli-service serve --open src/project1/main.ts",
    "build:project1": "vue-cli-service build --dest dist/project1 --open src/project1/main.ts",
  }
}

Проектов, разумеется, мб сколько угодно. Для более удобного доступа к shared модулю можно использовать аласи (tsconfig.json)

{
    "paths": {
      "@shared/*": ["src/shared/*"]
      }
}

Создадим вышеупомянутый компонент HelloWorld.vue в нашем shared приложении.

project
    - src
     - shared
      - components
       - HelloWorld.vue

Теперь его можно использовать внутри project1 src/project1/view/App.vue

<template>
  <HelloWorld msg="Another way to use shared dependencies" />
</template>

<script lang="ts">
import { Options, Vue } from "vue-class-component";
import HelloWorld from "../shared/components/HelloWorld.vue";

@Options({
  components: {
    HelloWorld,
  },
})
export default class App extends Vue {}
</script>

Запустим и убедимся что все работает npm run serve:project1

В заключение

Материал, согласно идеям Маркса, был частично позаимствован отсюда и отсюда. Возможны проблемы с относительными путями через @/ при использовании 2 подхода. На момент публикации статьи устранить их не удалось.

Пример можно потрогать тут

javascript
npm
VueJS
2
3011