I have setup hot reloading and dynamic loading of my vuex modules.
store.js file - hot update section
if (module.hot) {
// accept actions and mutations as hot modulesLoader
module.hot.accept([
'./store/modules/index.js',
'./store/helpers/global-actions',
'./store/helpers/global-mutations',
...modulePaths,
// './store/helpers/global-actions',
], () => {
let newModules = require('./store/modules').modules
store.hotUpdate({
actions: require('./store/helpers/global-actions'),
mutations: require('./store/helpers/global-mutations'),
modules: newModules,
})
})
}
modules/index.js file
const requireModule = require.context('.', true, /index.js$/)
const modules = {}
const modulePaths = []
requireModule.keys().forEach(fileName => {
if (fileName === './index.js') {
modulePaths.push(fileName.replace('./', './store/modules/'))
return
} else {
let moduleName = fileName.match(/(?<=\/)\w*(?=\/)/g)[0]
modulePaths.push(fileName.replace('./', './store/modules/'))
modules[moduleName] =
{
namespaced: false,
...requireModule(fileName),
}
}
})
export {modulePaths, modules}
Basically what this code does is loading folders with index.js file as modules (where module name is foldername) dynamically.
If I update module actions or getters or mutations everything works as expected I do get new actions added to store as well as mutations, when either of modules is updated.
The only thing I can't get to work is to get modules state changed on update. So if I change modules state it does not get reflected. Is it a normal behaviour? Or am I doing something wrong?
Related
I have an action called fetchUserPermissions which returns a permission set from an api endpoint through axios. This action is run trough another action called init, which is run automatically trough the utility dispatchActionForAllModules which basically looks for the init action in all modules/submodules and runs it. This part seems to work fine, but it seems like the endpoint returns html (?) instead of a JSON response. The returned response looks something like:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Title</title> </head> <body> <noscript> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <script type="text/javascript" src="http://0.0.0.0:8080/bundle.js" ></script> </body> </html>
It's important to know that i run Vue through Django using the webpack_loader package. The above html is the index.html that serves Vue.
However, when i dispatch the action in the created() hook of an component it works as expected, and returns something like { "id": 1, "is_staff": true, "is_superuser": true, "permissions": [ "view_user" ], "group_permissions": [ "has_users_export" ] }.
The dispatchActionForAllModules utility is basically copy paste from the vue-enterprise-bolierplate repo.
How can i make sure it returns a JSON response as expected?
state/store.js
import Vue from 'vue'
import Vuex from 'vuex'
import dispatchActionForAllModules from '#/utils/dispatch-action-for-all-modules'
import modules from './modules'
Vue.use(Vuex)
const store = new Vuex.Store({
modules,
strict: process.env.NODE_ENV !== 'production',
})
export default store
// automatically run the `init` action for every module,
// if one exists.
dispatchActionForAllModules('init')
state/modules/index.js
import camelCase from 'lodash/camelCase'
const modulesCache = {}
const storeData = { modules: {} }
;(function updateModules() {
const requireModule = require.context(
// Search for files in the current directory.
'.',
// Search for files in subdirectories.
true,
// Include any .js files that are not this file or a unit test.
/^((?!index|\.unit\.).)*\.js$/
)
// For every Vuex module...
requireModule.keys().forEach((fileName) => {
const moduleDefinition =
requireModule(fileName).default || requireModule(fileName)
// Skip the module during hot reload if it refers to the
// same module definition as the one we have cached.
if (modulesCache[fileName] === moduleDefinition) return
// Update the module cache, for efficient hot reloading.
modulesCache[fileName] = moduleDefinition
// Get the module path as an array.
const modulePath = fileName
// Remove the "./" from the beginning.
.replace(/^\.\//, '')
// Remove the file extension from the end.
.replace(/\.\w+$/, '')
// Split nested modules into an array path.
.split(/\//)
// camelCase all module namespaces and names.
.map(camelCase)
// Get the modules object for the current path.
const { modules } = getNamespace(storeData, modulePath)
// Add the module to our modules object.
modules[modulePath.pop()] = {
// Modules are namespaced by default.
namespaced: true,
...moduleDefinition,
}
})
// If the environment supports hot reloading...
if (module.hot) {
// Whenever any Vuex module is updated...
module.hot.accept(requireModule.id, () => {
// Update `storeData.modules` with the latest definitions.
updateModules()
// Trigger a hot update in the store.
require('../store').default.hotUpdate({ modules: storeData.modules })
})
}
})()
// Recursively get the namespace of a Vuex module, even if nested.
function getNamespace(subtree, path) {
if (path.length === 1) return subtree
const namespace = path.shift()
subtree.modules[namespace] = {
modules: {},
namespaced: true,
...subtree.modules[namespace],
}
return getNamespace(subtree.modules[namespace], path)
}
export default storeData.modules
state/modules/users.js
import axios from 'axios'
export const state = {
requestUserPermissions: [],
}
export const mutations = {
'FETCH_USER_PERMISSIONS' (state, permissions) {
state.requestUserPermissions = permissions
},
}
export const actions = {
init: ({ dispatch }) => {
dispatch('fetchUserPermissions')
},
fetchUserPermissions: ({ commit }) => {
axios.get('user/permissions/').then(result => {
console.log(result.data)
commit('FETCH_USER_PERMISSIONS', result.data)
}).catch(error => {
throw new Error(`API ${error}`)
})
},
}
utils/dispatch-action-for-all.modules.js
import allModules from '#/state/modules'
import store from '#/state/store'
export default function dispatchActionForAllModules(actionName, { modules = allModules, modulePrefix = '', flags = {} } = {}) {
// for every module
for (const moduleName in modules) {
const moduleDefinition = modules[moduleName]
// if the action is defined on the module
if (moduleDefinition.actions && moduleDefinition.actions[actionName]) {
// dispatch the action if the module is namespaced, if not
// set a flag to dispatch action globally at the end
if (moduleDefinition.namespaced) {
store.dispatch(`${modulePrefix}${moduleName}/${actionName}`)
} else {
flags.dispatchGlobal = true
}
}
// if there are nested submodules
if (moduleDefinition.modules) {
// also dispatch action for these sub-modules
dispatchActionForAllModules(actionName, {
modules: moduleDefinition.modules,
modulePrefix: modulePrefix + moduleName + '/',
flags,
})
}
}
// if this is at the root ant at least one non-namespaced module
// was found with the action
if (!modulePrefix && flags.dispatchGlobal) {
// dispatch action globally
store.dispatch(actionName)
}
}
The computed property in the component gets the data from store directly, like:
permissions() {
return this.$store.state.users.requestUserPermissions
}
In a large code base, there are await import statements like this
const { "default": MenuView } = await import('./menu/MenuView');
const { "default": MenuViewModel } = await import('./menu/MenuViewModel');
Here's a larger context:
import { View } from 'backbone.marionette';
import RivetsBehavior from 'behaviors/RivetsBehavior';
import tpl from './Mask.HeaderView.html';
import './Mask.HeaderView.scss';
export default View.extend({
behaviors: [RivetsBehavior],
template: tpl,
regions: {
menu: ".mask-menu"
},
async onRender() {
const { "default": MenuView } = await import('./menu/MenuView'); // <---------------
const { "default": MenuViewModel } = await import('./menu/MenuViewModel'); // <-----
const oMenuViewModel = new MenuViewModel();
oMenuViewModel.setOptions(this.options);
this.showChildView('menu', new MenuView({
model: oMenuViewModel
}));
}
});
I moved the imports to the top of the file:
import { View } from 'backbone.marionette';
import RivetsBehavior from 'behaviors/RivetsBehavior';
import tpl from './mask.HeaderView.html';
import './mask.HeaderView.scss';
import MenuView from './menu/MenuView'; // <---------------------------- here
import MenuViewModel from './menu/MenuViewModel'; // <------------------- here
export default View.extend({
behaviors: [RivetsBehavior],
template: tpl,
regions: {
menu: ".maskn-menu"
},
async onRender() {
// const { "default": MenuView } = await import('./menu/MenuView'); <------------ no
// const { "default": MenuViewModel } = await import('./menu/MenuViewModel'); <-- no
const oMenuViewModel = new MenuViewModel();
oMenuViewModel.setOptions(this.options);
this.showChildView('menu', new MenuView({
model: oMenuViewModel
}));
}
});
Everything seems to work. But I am worried that I am missing something.
Questions
Why not simply place those await imports with the other imports at the top of the file?
Could this be performance related? In the example there are only 2 await-imports but the code base has e.g. one file with 60 functions and in each function there are 2 await-imports. Each function imports something different.
Could this be UI experience related (i.e. avoid blocking the UI).
Everything should work fine with static imports as well. Both codes should work.
But when you import those modules dynamically, it can be a bit better in this case:
The modules get imported statically, they will be executed before executing the module that imported them. Opposed to that, when the import is in the onRender function, the imported modules are evaluated first time the function is called.
That way, we can defer the execution of the imported modules until they are really needed. If onRender is never called, these modules aren't get imported at all.
So, although your version will work as well, the original can be a bit better (but it also depends on the way how the onRender function is called).
I'm working with a modular vue application that registers the modules at compile time. Please see the code below -
app.js
import store from './vue-components/store';
var components = {
erp_inventory: true,
erp_purchase: true,
};
// Inventory Module Components
if (components.erp_inventory) {
// erp_inventory.
store.registerModule('erp_inventory', require('./erp-inventory/vue-components/store'));
// erp_inventory/product_search_bar
store.registerModule([ 'erp_inventory', 'product_search_bar' ], require('./erp-inventory/vue-components/store/products/search-bar'));
}
./erp-inventory/vue-components/store/index.js
export default {
namespaced: true,
state() {
return {};
},
getters: {},
actions: {}
}
./erp-inventory/vue-components/store/products/search-bar/index.js
export default {
namespaced: true,
state() {
return {
supplier_id
};
},
getters: {
supplier_id: (state) => {
return state.supplier_id;
}
},
actions: {
set_supplier_id({ commit }, supplier_id) {
commit('set_supplier_id', supplier_id);
}
},
mutations: {
set_supplier_id(state, supplier_id) {
state.supplier_id = supplier_id;
}
}
}
When I use context.$store.dispatch('erp_inventory/product_search_bar/set_supplier_id', e.target.value, {root:true}); to dispatch the action in search-bar/index.js, vue is unable to find the namespace stating [vuex] unknown action type: erp_inventory/product_search_bar/set_supplier_id
I've read the documentation of vuex and dynamic modules and even though I've set namespaced: true, in each store, this problem persists. After dumping the store of my app I found that namespaced was never being set for registered modules (see image below).
Unless I'm doing something wrong, could it be a bug?
You have to use require(....).default, otherwise you won't get the default export pc fro your ES6 module file, but object by webpack that's wrapping it.
Currently I am loading all of my Vue components with require.context, this searches my components directory with a regex for .vue files. This works fine but I would like to load async components as well with dynamic imports.
Currently when I use require.context all files get loaded so even If I want to use a dynamic import my file is already loaded and nothing happens.
I need a way to exclude certain files from my require.context call. I cannot dynamically create a regex because this does not work with require.context.
// How I currently load my Vue components.
const components = require.context('#/components', true, /[A-Z]\w+\.vue$/);
components.keys().forEach((filePath) => {
const component = components(filePath);
const componentName = path.basename(filePath, '.vue');
// Dynamically register the component.
Vue.component(componentName, component);
});
// My component that I would like to load dynamically.
Vue.component('search-dropdown', () => import('./search/SearchDropdown'));
It seems the only way to do this is either manually declare all my components, which is a big hassle.
Or to create a static regex that skips files that have Async in their name. Which forces me to adopt a certain naming convention for components that are async. Also not ideal.
Would there be a better way to go about doing this?
const requireContext = require.context('./components', false, /.*\.vue$/)
const dynamicComponents = requireContext.keys()
.map(file =>
[file.replace(/(^.\/)|(\.vue$)/g, ''), requireContext(file)]
)
.reduce((components, [name, component]) => {
components[name] = component.default || component
return components
}, {})
Works with Vue 2.7 and Vue 3.
The lazy mode forces requireContext to return a promise.
const { defineAsyncComponent } = require('vue')
const requireContext = require.context('./yourfolder', true, /^your-regex$/, 'lazy')
module.exports = requireContext.keys().reduce((dynamicComponents, file) => {
const [, name] = file.match(/^regex-to-match-component-name$/)
const promise = requireContext(file)
dynamicComponents[name] = defineAsyncComponent(() => promise)
return dynamicComponents
}, {})
You can also use defineAsyncComponent({ loader: () => promise }) if you want to use the extra options of defineAsyncComponent.
Vuex complains that a new instance of the store cannot be created without calling Vue.use(Vuex). While this is okay generally, I am fiddling with the idea of writing a backend/frontend using the same store. Anybody know the answer?
Thanks.
TL;DR you can perfectly use Vuex in node (without a browser), even for unit testing. Internally, though, Vuex still uses some code from Vue.
You can't use Vuex without Vue. Because:
Vuex checks for the existence of Vue.
Vuex depends largely on Vue for its reactivity inner workings.
That being said, you do require Vue, but you don't require a Vue instance. You don't even require the browser.
So yes, it is pretty usable in the server-side, standalone.
For instance, you could run it using Node.js as follows:
Create a sample project:
npm init -y
Install the dependencies (note: axios is not necessary, we are adding it just for this demo):
npm install --save vue vuex axios
Create a script (index.js):
const axios = require('axios');
const Vue = require('vue');
const Vuex = require('vuex');
Vue.use(Vuex);
const store = new Vuex.Store({
strict: true,
state: {name: "John"},
mutations: {
changeName(state, data) {
state.name = data
}
},
actions: {
fetchRandomName({ commit }) {
let randomId = Math.floor(Math.random() * 12) + 1 ;
return axios.get("https://reqres.in/api/users/" + randomId).then(response => {
commit('changeName', response.data.data.first_name)
})
}
},
getters: {
getName: state => state.name,
getTransformedName: (state) => (upperOrLower) => {
return upperOrLower ? state.name.toUpperCase() : state.name.toLowerCase()
}
}
});
console.log('via regular getter:', store.getters.getName);
console.log('via method-style getter:', store.getters.getTransformedName(true));
store.commit('changeName', 'Charles');
console.log('after commit:', store.getters.getName);
store.dispatch('fetchRandomName').then(() => {
console.log('after fetch:', store.getters.getName);
});
Run it:
node index.js
It will output:
via regular getter: John
via method-style getter: JOHN
after commit: Charles
after fetch: Byron