Vue-storefront: New modules API (final)

Created on 25 Jun 2019  路  36Comments  路  Source: DivanteLtd/vue-storefront

New, simplified modules API

Broader explaination still needs to be written but in short words this change will enable;

Inspiration is taken from new Vue's functional API and nuxt modules.

  1. Less abstraction with direct access to store, router so better learning curve and sticking to native Vue APIs to not limit customization posibilities with our limited wrappers on it. Also less code to maintain ;)
  2. Much simplier API based on single function
  3. Ability to hook into various points of application lifecycle
  4. Separation of module APIs and ones that can be used inside modules from the core package.
  5. @vue-storefront/module can now be a separate npm package used in VS modules packages as dependency.
  6. Having this will allow us to enable packaging fo VS modules as npm packages that doesn't require tyepscript.
  7. We have control over what can be used in modules = no need to worry about changing APIs in core that could break modules
  8. Ability to register modules lazily which will gain huge performance boost. If module is registered lazily it can't access some of the hooks like onAppInit.
  9. Current Vuex extending API is rough and not intuitive so it needed to be rewritten anyway
  10. Ability to codesplit VS modules config and pass config to module
  11. Ability to define extension points in module thanks to config

New API

import { extendStore, isServer } from '@vue-storefront/module/helpers' // helpers that can be sued inside module
import { afterAppInitHook  } from '@vue-storefront/module/hooks' // List of events  and hooks
import { cartVuexModule } from './store/cart.js' // Vuex module that will extend cart module
import { newVuexModule } from './store/newModule.js'// new Vuex module to be registered
import { newRoutes } from './routes/newRoutes.js'

export default function (app, router, store, config, appConfig) => {

  // This is how you extend the router, native Vue API
  router.addRoutes(newRoutes)
  router.beforeEach((to, from, next) => { next() })

  // This is how you extend the Vuex store, native Vue API
  store.registerModule('newModule', newVuexModule)

  // This is how you extend currently existing Vuex module, much simpler API, just pass Vuex module and it will be merged with module that has same name
  extendStore('cart', cartVuexModule)

  // This is how you can access Vue Storefront hooks
  afterAppInitHook(() => {
    console.log('App has just been initialized')
  })
}

Inside module's init function we have acces to:

  • app instance (Vue instance)
  • router insatnce
  • Vuex store instance
  • module config passed during registration
  • VS config

What else:

  • You can call module helper functions outside of modules. This will let you to do simple modifications in your theme without a need to wrap it into VS module. For instance if you want to just extend Vuex module from your theme instead of creating a lot of boilerplate you can simply do this
// theme/index.js
import { extendStore } from '@vue-storefront/module/helpers' 
import { newCart } from './cart.js'

extendStore('cart', newCart)

Much more elegant isn't it?

-Thanks to config you can define extension points in your module, i.e. let people override your hooks

// module
export default function (app, store, router, config, appConfig) {
  onAppInit(() => {
    if (config.onAppInit) {
      config.onAppInit()
    } else {
      // regular hook
    }
  })
}
// registration
registerModule(someModule, {
  onAppInit: () => { console.log('Hello') }
})

Registration

Whenever you want to register VS module you can do it by using registerModule function and passing optional config. This will let us to codesplit module configs.

import { registerModule } from '@vue-storefront/module'
import { RecentlyViewedModule } from './modules/recently-viewed'

registerModule(RecentlyViewedModule, {
    productsCount: 10
})

What about backward compatibility

Porting old Vue Storefront modules to new ones will be available only through changing index file of any module. There will be also a method to port old module to new one via helper function as simple as

import { legacyModuleWrapper } from '@vue-storefrpnt/module/helpers'
import { registerModule } from '@vue-storefrpnt/module/helpers'
import { legacyModule } from './legacyModule'

registerModule(legacyModuleWrapper(legacyModule))

What will happen with old api

Old way of Vue Storefront modules registration will still be possible and won't be depreciated until Vue Storefront 2.0. After this there will still be a legacyModuleWrapper function so old modules still can be used (but they can't benefit from new features)

In plans

Ability to extend express server and webpack config.

Each module could have a server.js file in a root that will export webpack config object and express server.
For webpack it will work exactly the same as current extension model from themes.
For server it will work as current extension model in src/server

// module/server.js
exports.webpack = function (config) {
  // do modifications
  return config
}

exports.server = function (app) { 
  // do sth with express instance
}

If you'd like to make use of server utilities of a given module you just need to register server side part of the module in VS config.

// config/local.json
"modules": {
  "serverSideModules": ['absolute/path', 'package-name']
}

Please note that no matter if client part fo the module is async or not server side part will be registered during app initialization so we have 2 entry points for every module - index.ts for client (can be loaded lazily) and server.js for webpack/express

RFC

Most helpful comment

So assuming that we have extension point in theme

<div ext-payment-method />

you can extend it from the inside of module (or anywhere else) just by writing

Vue.extend('[ext-payment-method], Component)

This is cool idea @filrak as a VSF user I want to have this feature! :)

All 36 comments

This is very promising!
First things in my mind that can be improved in the way the modules are integrated now are:
Module config settings in app config (local.json) - separate config file for module which will be merged to apps config
A way to register the module without adding three lines in src/modules/index.ts like it is now

I'm curious how you see the listeners for events from other modules. Does this also change in this new setup?

I like this new design a lot, it's great. I think we should just discuss the list of the available hooks. I think that we should have a hooks for general state changes for the whole app at this level so it should be like:

onServerEntry
onClientEntry
onStoreViewChanged
onUserLoggedIn
onUserLoggedOut

@DaanKouters thanks for your feddback!

Module config settings in app config (local.json) - separate config file for module which will be merged to apps config

This is why now registerModule can take config as a parameter. Not only module config object (that can sometimes be really big) can be separated from core functionalities but also codesplitted ;)

A way to register the module without adding three lines in src/modules/index.ts like it is now

Maybe it wasn't clear from RFC but basically you just need to call registerModule function whenever and whereever you want and that's all. For server -side module you'd probably need to pass paths to config but this is not a part of this RFC yet

@pkarw I agree ;) We should think about list of events that is needed in VS. Maybe also beforePlaceOrder(order) : order so people can hook into order object?

I added section about server side modules

In my opinion, the new API looks much better. Modules as npm packages is an amazing idea. However, I am mostly interested in Vue Storefront hooks. What do you think about hooks like:

onRemoveFromCart
onAddToWishlist
onRemoveFromWishlist

I had to do tricky things with EventBus and debounce (the event was emitted multiple times) for catching those events here.

I would love to see a big bunch of useful, well-documented hooks in new modules API.

@Fifciu that's a very good idea.
I'm looking for a more efficient way also, f.e. we listen to the payment-method-changed event in one of our modules, but every time we catch it we have to unregister the previous registered in there to not register multiple times the same one.
Actually it works fine now, but i think this can be improved in a more efficient way.

@filrak could you please tell how you see the implementation of markup?
I mean especially the split between module logic en theme markup, hope you understand what i mean.

Right now theme and modules are pretty mixed inside one another. Don't know for sure if anything is possible with slots or something to add module components to themes without specifically change theme files.
Something similar to Magento layout XML for example and then the reference container part to add components from modules.

Maybe you already talked about it in Storefront UI, but i still didn't have enough time to dive into that, it's a real shame i know :(

Whole idea looks really good!
My only real concern is about modules registration and extending. Also we should have typing in mind. We need to answer to these questions to have clear and common understanding about logic:

  1. What when i'll invoke once again module registration? For example on another page.
  2. What if i'll do this with different config?
  3. Same with extending - what if i'll extend it twice, will the changes replace itself?
  4. How does module architecture will look?

I was thinking back in days for some approaches on this topic so i'll just put what was on my mind for further brainstorming

First approach

My first idea was something like plugins.

Vue.use(VSF, {
// core config
})

Then we could setup modules in one place

import { RecentlyViewedModule } from '@vue-storefront/modules/recently-viewed'

VSF.addModule(RecentlyViewedModule, { // lazy by default
// config
})

then we could be a proxy for module like

VSF.recentlyViewed.someAction() // at this point VSF would register this module

VSF.recentlyViewed.getReviews

module could even not use vuex, if simple enough then it could be just Observable api for its state. And module, and its Actions would be then typed, everything would be clear for IDE.

Second approach

Second approach is more like proposed above. But module can consume hooks and have their own hooks. Thanks to this core is clear (no unused hooks).

Module registration

import { registerModule } from '@vue-storefront/module'
import { RecentlyViewedModule } from '@vue-storefront/modules/recently-viewed'

registerModule(RecentlyViewedModule, {
    productsCount: 10
})

Lazy module registration

import { registerModule } from '@vue-storefront/module'
const ReventlyViewed = () => import('@vue-storefront/modules/recently-viewed')

registerModule(RecentlyViewed.RecentlyViewedModule, {
    productsCount: 10
})

Then in module when we need to use storefront hook

import { onAppInit } from '@vue-storefront/hooks'

// inside module function
onAppInit(() => {
  // use onAppInit
})

but i would also allow to use hooks from other modules. If module is not registered it's not bad - we just won't have hook. Then we would have cleaner api and clear hooks

for example we could have Cart and User module and first should fetch cart data when user log in. So in Cart module setup would be something like this

import { onUserLogin } from '@vue-storefront/module/user/hooks'

// inside cart module function
onUserLogin(async (userData) => {
  const serverCart = await loadServerCart(userData.id)
  // and merge it with local cart
})

Then we can have VSF hooks like

onServerEntry
onClientEntry

and others like

onUserLoggedOut
onAddToCart
onRemoveFromCart
onAddToWishlist
onRemoveFromWishlist

would be modules hooks. It allows us to easily adopt new modules and creates their own independent API.

And then every module have also clean docs like:

actions:
- login
- logout
- register
- edit

getters:
- isLoggedIn
- getUser
- getUserAddresses

hooks:
- onUserLogin
- onUserLogout

I added a section about module extension points. As module developer you can define extension points of your module (like possibility to alter some code parts) that can be passed to module config,

// module
export default function (app, store, router, config, appConfig) {
  onAppInit(() => {
    if (config.onAppInit) {
      config.onAppInit()
    } else {
      // regular hook
    }
  })
}
// registration
registerModule(someModule, {
  onAppInit: () => { console.log('Hello') }
})

@DaanKouters this is something that I though about a few times. In Theme 2.0 we want to define extension points in theme as special HTML attributes. Then Vue has extend method that let's you inject component into certain HTML element (works like el property of vue instance.

So assuming that we have extension point in theme

<div ext-payment-method />

you can extend it from the inside of module (or anywhere else) just by writing

Vue.extend('[ext-payment-method], Component)

I wanted to do separate RFC for this once I'll have more time to play with it in a new theme but if you asked here I'm happy to share my idea earlier

@patzick despite your questions, 2nd thing is (if i understand it correctly) exactly what is proposed.

My idea for hooks would be that a hook is essentially just putting a passed function to a FIFO queue. Then those functions are executed on top of function they are hooked into so appinit() function is essentially applyHooks(appInit()

Please keep in mind that we still need to have control over router guards and hooks in case we want to disable/override them.

@lukeromanowicz hence extension points part on propsal

Looks like a really good improvement over the current system, I really like how it uses inspiration from Vue and Nuxt module system, that should help adoption as users will be more familiar with the APIs already.

Having this will allow us to enable packaging fo VS modules as npm packages that doesn't require tyepscript.

This gets a big +1 from me as it's been one of the biggest pain points to get #2271 working, does this mean it will be up to module developers to compile TS on their end if they choose to use it?

Also, are hooks meant to replace the current event bus system? Right now it can be cumbersome to know what to use if you want to override or extend some parts of VS, some use Vuex actions, other the even bus, etc. Is the plan to eventually use hooks for these kind of things? Eg: when an order is placed instead of triggering a order-after-placed event would this use a hook instead?

@jahvi

does this mean it will be up to module developers to compile TS on their end if they choose to use it?

Not really. We will just have a boilerplate that compiles js to ts. The biggest problem here was the fact that modules were relaying on core functions and core is not a proper npm package yet so we can't make it a dependency of such boilerplate.

Now the API available for module creators will be a part of separate package - @vue-storefront/modules (or /module, not sure which naming makes more sense).

Also an idea on hooks implementation. Generally I see two types of them

  • listeners (perform functions collected from hooks at given moment, for example `onAppInit(fn))
  • mutators (hooks have access to passed value and can modify it, for example onPlaceOrder(order => fn))

In Listener Hook you export listener as hook to theme/modules and invoker to core to invoke functions passed to listener.

In Mutator Hook you export listener as hook that can modify value passed to mutator to theme/modules and invoker to wrap editable value`.

Please let me know if you think there is a need for more types of hooks and what do you think about this.

/**
  Listener hook just fires functions passed to listener function when invoker is executed.
  e. g. We want to listen for onAppInit event in various places of the application.
  Functions passed to this hook will be invoked only when invoker function is executed.
  Usually we want to use listener as a hook in app/modules and invoker in core.
 */
function createListenerHook () {
  const functionsToRun = []

  function listener (fn) {
    functionsToRun.push(fn)
  }

  function invoker () {
    functionsToRun.forEach(fn => fn())
  }

  return {
    listener,
    invoker
  }
}

/**
  Mutators work like listeners except they can modify passed value in hooks.
  e.g we can apply the hook mutator to object order that is returned before placing order
  now you can access and modify this value from hook (listener) returned by this function
 */
function createMutatorHook () {
  const mutators = []

  function listener (mutator) {
    mutators.push(mutator)
  }

  function mutator (rawOutput) {
    let modifiedOutput = null
    mutators.forEach(fn => {
      modifiedOutput = fn(rawOutput)
    })
    return modifiedOutput
  }

  return {
    listener,
    mutator
  }
}

export const { listener: onAppInit, invoker: appInitInvoker } = createListenerHook()

Good point @filrak - mutators/listeners are exactly covering all the situations we can have with events. However - wouldn't it be simpler if we ended up with just one type (mutator kind of listener) - not to frustrate the users on which type they should incorporate?

@pkarw for users it's only onEvent() . They are not aware of internals, it's just for us to build hooks

Update;

Convention for hooks is before/afterEventNameHook
so we can have:
beforePlaceOrderHook
afterPlaceOrderHook

So assuming that we have extension point in theme

<div ext-payment-method />

you can extend it from the inside of module (or anywhere else) just by writing

Vue.extend('[ext-payment-method], Component)

This is cool idea @filrak as a VSF user I want to have this feature! :)

And then every module have also clean docs like:

actions:
- login
- logout
- register
- edit

getters:
- isLoggedIn
- getUser
- getUserAddresses

hooks:
- onUserLogin

@patzick and @filrak this is awesome. This will be really transparent to the users

That would be really great indeed

@DaanKouters this is something that I though about a few times. In Theme 2.0 we want to define extension points in theme as special HTML attributes. Then Vue has extend method that let's you inject component into certain HTML element (works like el property of vue instance.

So assuming that we have extension point in theme

<div ext-payment-method />

you can extend it from the inside of module (or anywhere else) just by writing

Vue.extend('[ext-payment-method], Component)

I wanted to do separate RFC for this once I'll have more time to play with it in a new theme but if you asked here I'm happy to share my idea earlier

That would be really nice feature

This looks really good so far! Since VS is planning to use hooks does it mean we need to wait for Vue to release support for them first or can they be used now?

@jahvi those hooks are not connected to Vue ones at all. We have our own implementation :)

Can we include Auto registration and auto removal for modules?

@DaanKouters @pkarw @patzick @jahvi do you think hooks api and extendStore should be part of @vue-storefront/modules package or a new one @vue-storefront/utils. Technically those could be used also in theme so I'm wondering if they should be a different package

hi @filrak,
i think they should sit in @vue-storefront/utils because indeed it's not only scoped to modules. That's my opinion :)

Ok, and how about keeping them in modules package and providing a best practice to create projectModule per project which will encapsulate all project-specific overrides and additions?

@filrak I'm not a massive fan of util classes as they can get out of control (I mean almost everything can be considered an util).

I can't think of a use case where you'd need to use the proposed here outside the context of a module but I might be wrong.

Ok, and how about keeping them in modules package and providing a best practice to create projectModule per project which will encapsulate all project-specific overrides and additions?

This is tricky to get right and come up with a best practise.
I agree with @jahvi that's it unclear when something is a util, because you can explain almost anything as a util.

But the projectModule could solve that i think. How far do you want to take this? It sounds really good, for example we can also place the projectspecific config json in there.
And a simple way to see what modules are active in the project.

I'm closing the issue. New API is now merged into develop

I might have found a bug with this.

Before this you could overwrite actions or mutations.

Now when you try to overwrite a like an action he calls the original and the new one which causes to get an array as a response when two functions return there values instead of just one response.

Is that the way it should now work or is it a bug? @filrak

You . mean old modules api from master, yup?

Yea the old way to extend the modules.

@filrak I've been converting https://github.com/kodbruket/vsf-storyblok-sync to the new module api and it's nice. The latest code isn't pushed as of writing this comment.

However I can't really use the router the api provides. I need to import the RouterManager and add routes using that so that the routes are mapped in multi store mode etc.

It's fine, but I just thought I'd mention it so it could be added to the docs.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

paulocoghi picture paulocoghi  路  5Comments

yuriboyko picture yuriboyko  路  3Comments

jorkvist picture jorkvist  路  5Comments

revlis-x picture revlis-x  路  3Comments

kyvaith picture kyvaith  路  5Comments