主要诉求和存在的问题
- 这是一个包含多个模块的大型前端项目,过往我们采用多个单页面来隔离各个模块,现希望只使用一个单页面包含所有模块
- 打包
- 采用多个单页面是为了加快打包速度,使各个模块的发布互不影响。如何解决模块打包速度问题,热打包能否胜任?
- 采用多个单页面时,公共组件改动必须对所有模块进行重新打包。在单页面应用中如何避免公共组件的修改导致所有模块重新打包?
- @vue/cli创建的项目在打包时各个chunk之间是相互独立的,但是都共同会影响到app.js这个文件。即,一个文件改变只会影响到同步引用该文件的模块,不会影响到异步引用的。
- 如何实现同一库不同版本的共存
- @vue/cli创建的项目原生打包规律
- 一个组件只在一个chunk中使用,被打包到chunk中
- 在多个chunk中使用时,会根据组件的大小和关联的chunk包的多少自动判断是否需要打包成公共依赖
- 在blund中出现的话,不管有没有在其他chunk出现都会被打包到app.js中
- 问题:公共组件和函数在chunk中使用,如何强制打包到app.js中?采取强制归入blund后是否能加快打包速度?
- 多语言
- 在只有几种语言的项目中,把语言内嵌各个组件中比较方便代码管理
- 但是它无法独立打包各类语言,无法实现按需加载的要求。当然可以开发webpack插件,用来处理 i18n 标签实现分语言集合,但这违背了基于组件的本地化本身的原理,而且还需要改动调用路径,这并不划算
- 在需要多种语言时,为了压缩语言包体积,采用按需加载。同时为了方便管理散落在各个模块内的语言文件,还需要实现自动构建。
- 更复杂的情况是按用户当前模块、倾向语言需要进行加载,但是这需要权衡多次请求的开销和语言包大小开销。
- 最好的方式是实现服务端渲染 vue-i18n-extensions
- 路由
- 状态
- 如何实现sessionStorage和状态的同步
- 如何跨框架共享状态
目录结构
- assets 静态文件目录
- components 全局公共组件
- langs 全局公共组件多语言
- zh.json
- other.json
- Container.vue (详)公共组件代码,这是一个容器组件用于路由嵌套,把所有功能点路由都平铺容易导致路由寻址缓慢。这里依据功能块来嵌套路由
- other.vue
- fun 全局公共函数
- buildLang.js (详)构建i18n messages对象的函数
- other.js
- langs
- zh.js 使用buildLang搜集散落在模块中的zh语言包,每种语言需要一个文件
- other.js
- modules
- one 大功能模块
- components 大功能模块中的公共组件
- fun 大功能模块中的函数
- views 大功能模块中的各个功能点
- about 功能点about
- lang 功能点i18n
- zh.json
- other.json
- About.vue 功能点的代码
- Other.vue
- routes.js 功能点的路由
- home 功能点home
- 同about
- moduleRoutes.js 功能块路由
- store.js 功能块状态,待商榷
- other 其他大功能块
- 同one
- App.vue
- i18n.js 按浏览器当前语言或用户配置加载langs文件中的多语言,并设置对应的其他语言相关项
- main.js
- router.js 收集各个功能块的路由生成根路由,并注册如vue实例
- store.js 根状态,待商榷
多语言
- i18n.js 根据浏览器当前语言或用户保存在缓存中的语言按需加载,并设置语言相关项
import Vue from "vue";
import VueI18n from "vue-i18n";
Vue.use(VueI18n);
const loadedLanguages = []; // 已经加载过的语言
function getNavigatorLanguage() {
const lang = navigator.language;
return lang.slice(0, 2);
}
export async function loadLanguage(lang) {
if (i18n.locale !== lang) {
if (!loadedLanguages.includes(lang)) {
try {
const msgs = await import(
/* webpackChunkName: "lang-[request]" */
`@/langs/${lang}`
);
i18n.setLocaleMessage(lang, msgs.default);
loadedLanguages.push(lang);
return setLanguage(lang);
} catch (error) {
return loadLanguage(
process.env.VUE_APP_I18N_FALLBACK_LOCALE || "en"
);
}
}
return setLanguage(lang);
}
return lang;
}
function setLanguage(lang) {
i18n.locale = lang;
localStorage.setItem("lang", lang);
// axios.defaults.headers.common["Accept-Language"] = lang;
document.querySelector("html").setAttribute("lang", lang);
return lang;
}
const i18n = new VueI18n({
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || "en",
locale: "null"
});
loadLanguage(localStorage.getItem("lang") || getNavigatorLanguage());
export default i18n;
import buildLang from "@/fun/buildLang.js";
const langs = require.context("../", true, /en.json$/);
export default buildLang(langs);
// .env messages对象的结构是根据语言包路径生成的,但是很多路径片段没有意义,所以添加了过滤项常数。
// 例如:src/modules/one/views/about/langs/zh.json 内的 {message:"一些消息"} 在使用时应该是$t('one.about.message')
VUE_APP_I18N_MESSAGES_FILTER_PATHS=[ "modules", "views", "langs"]
// buildLang.js
const filterPaths = JSON.parse(process.env.VUE_APP_I18N_MESSAGES_FILTER_PATHS);
function setMessages(pathArr, index, langValue, messagesPointer) {
const path = pathArr[index];
if (++index === pathArr.length - 1) {
messagesPointer[path] = langValue;
} else {
if (!messagesPointer[path]) {
messagesPointer[path] = {};
}
return setMessages(pathArr, index, langValue, messagesPointer[path]);
}
}
export default function(langs) {
const messages = {};
langs.keys().forEach(key => {
const pathArr = key.split("/").filter(path => {
return !filterPaths.includes(path);
});
setMessages(pathArr, 1, langs(key), messages);
});
return messages;
}
路由
收集散落在各个功能块的路由,生成路由表
- router.js 构建路由,不是特别完善,需要增加对404的处理。
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
const modulesRoutes = require.context("./modules", true, /moduleRoutes.js$/);
const routes = [];
modulesRoutes.keys().forEach(key => {
routes.push(modulesRoutes(key).default);
});
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes
});
export default router;
- src/modules/one/moduleRoutes.js 功能块的根路由,由各个功能块自行维护。也可以自动生成,视情况而定
import Container from "@/components/Container";
const modulesRoutes = require.context("./views", true, /routes.js$/);
const children = [];
modulesRoutes.keys().forEach(key => {
children.push(...modulesRoutes(key).default);
});
export default {
path: "/one",
component: Container,
children
};
- src/modules/one/views/about/routes.js
- 一个功能点一般由一人负责,这个功能点可能包含多个页面,它对外应该是一个数组。
- 功能点得路由应该返回一个数组,这样可以支持平铺所有页面的方式,也可以采用嵌套的方式
- 采用同步引用还是异步引用应当根据使用情况和代码大小决定,一般一个功能模块使用一个chunk就足够了
- 路由相关的面包屑应该在meta中体现,而不是依赖路由嵌套关系
// 采用嵌套的方式会使寻址更快
const routes = [
{
path: `home`, // 功能块,功能块一般是依据业务点集合的,由负责的程序员自行定义
component: HomeNav, // 该功能块拥有类似的结构
children: [
{
path: "",
name: "incHome",
component: Home
},
{
path: ":id",
name: "incHomeDetail",
component: HomeDetail,
props: true
}
]
},
];
export default routes;
- Container.vue 容器组件,当路由需要依据功能块分层时用来包裹children
export default {
render() {
return <router-view />;
}
};
VUE_APP_I18N_MESSAGES_FILTER_PATHS=["langs", "views", "modules"]
VUE_APP_I18N_FALLBACK_LOCALE=en
根据用户菜单权限动态生成路由