如何使用 Lavas 构建 MPA 项目
Lavas 目前支持两种渲染模式,分别是服务端渲染 (SSR) 和 浏览器端渲染。 而浏览器端渲染时,整个项目构建完成后只有一个 HTML 入口 index.html,因此这时本质上等价于单页应用,即 SPA。
从小型站点的开发需求来说,SPA 和 SSR 已经能够覆盖绝大部分。让我们更进一步,考虑如下两种情况:
- 整个站点的一部分需要使用 SSR 模式渲染,另一部分使用 SPA 模式渲染。
 - 整个站点均为浏览器端渲染,但存在多个入口 HTML,即多页应用 (MPA)。
 
如何使用 Lavas 来满足此类需求呢?
解决思路
我们可以把 Lavas 项目看做一个单独的单元,或称入口。
对于上述两种情况,都可以认为由多个入口组成:
- 情况 1,相当于一些入口采用 SSR 模式,另一些入口采用 SPA 模式
 - 情况 2,相当于项目中存在多个 SPA 模式的入口
 
因此,我们把问题归结为:多个 Lavas 项目如何整合到一起?
在 Lavas 构建完成后,SPA 项目本质上是一些静态页面,包含一个入口 index.html 和其他静态资源;SSR 项目本质上是一个 express (也可以是 koa )中间件。因此通过架设一个 express 服务器可以很方便分别对两者进行整合。
实现方式
为了表述方便,我们来创建一个简单但可能很实用的需求,通过解决这个需求来学习如何整合。
示例需求
我们假设开发者存在这样的开发需求:一个电商站点从业务模块角度能分成两部分,分别是:
/user/*部分,用户信息浏览/注册/修改等等相关,采用 SPA 模式渲染。一般这类用户信息敏感的内容不需要考虑 SEO,也就不需要 SSR。- 剩余部分,是站点的主要内容,例如 
/展示站点首页,/detail/*查看商品详情,/search/*进行商品搜索等等。这部分考虑到 SEO 需求,使用 SSR 模式进行渲染。 
我们假设开发者已经分别为两者开发了单独的 Lavas 应用。前者名为 lavas-user,后者名为 lavas-main。我们需要这么几个步骤:
- 修改基础路由互相区分
 - 提取共享模块避免重复 (可选)
 - 配置服务器
 - 构建
 
修改基础路由
观察示例需求的两个模块,lavas-user 拥有明显的 URL 特征 ( /user 开头),而 lavas-main 没有。因此我们修改 lavas-user 的 base,lavas-main 不作修改。
打开 lavas-user/lavas.config.js 配置文件的build 段的 publicPath 以及 router 段的 base,均修改为 /user/ (不要遗漏最后的 / !),如下:
12345678910111213// ...
build: {
    // ...
    publicPath: '/user/'
},
router: {
    mode: 'history',
    base: '/user/',
    pageTransition: {
        enable: false
    }
}
// ...info
base配置项是 vue-router 的一个配置项,用以设定基准路由。修改后的 lavas-main,原本使用/view的路由就变成了/user/view, 原本使用/register就变成了/user/register,以此类推。 为了配合base, 用以管理静态资源路径前缀的配置项publicPath也应做相同的修改,否则会导致系统找不到静态资源而报错。
提取共享模块 (可选)
如果 lavas-main 和 lavas-user 两个模块都引用了相同的内容,开发者又对重复代码无法接受,可以考虑将共同的代码抽离出来。
举例来说,由 lavas init 初始化的项目都会有 /components 目录,里面会有共同使用的组件 (离线通知,Service Worker 更新通知和页面切换进度条)。如果要把这块提取出来的话,我们可以通过 webpack 的 alias 实现。
123456789lavas-project
├── components/
│   ├──OfflineToast.vue
│   ├──UpdateToast.vue
│   └──ProgressBar.vue
├── lavas-user/
│   └──lavas.config.js
└── lavas-main/
    └──lavas.config.jsLavas 为开发者提供了 alias 配置项(文档),修改 /lavas.config.js 即可。
12345678// ...
build: {
    alias: {
        base: {
            'common': path.resolve(__dirname, '../')
        }
    }
}将项目中使用到公共目录 components 中组件的地方改为以 common 开头 ,如下,如果我们将 UpdateToast 移到公共目录下
1import UpdateToast from 'common/components/UpdateToast';除了 components,其他的公用内容也可以提出到公共目录,同样使用 common 为前缀来引用,非常方便,并且项目会清晰。
配置服务器
最后一步是在两个 Lavas 服务之前搭建一个分发服务器,对不同的 URL 转发到不同的 Lavas 服务。以 express 为例,我们新建 server.js,内容如下:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960const path = require('path');
const express = require('express');
const app = express();
const historyMiddleware = require('connect-history-api-fallback');
const LavasCore = require('lavas-core-vue');
const port = 8080; // 对外端口
function registerSPA(url, dirPath) {
    if (url.endsWith('/')) {
        url = url.substring(0, url.length - 1);
    }
    // fix trailing slash (/user -> /user/)
    app.use('/', function (req, res, next) {
        let requestUrl = req.url.replace(/\?.+?$/, '');
        if (requestUrl === url) {
            req.url = url + '/';
        }
        next();
    });
    app.use(url, historyMiddleware({
        htmlAcceptHeaders: ['text/html'],
        disableDotRule: false // ignore paths with dot inside
    }));
    app.use(url, express.static(dirPath));
}
// SPA
registerSPA('/user', 'lavas-user/dist');
// NOT required when SSR is enabled
// app.listen(port, () => {
//     console.log('server started at localhost:' + port);
// });
// SSR
let core = new LavasCore(path.resolve(__dirname, 'lavas-main/dist'));
core.init('production')
    .then(() => core.runAfterBuild())
    .then(() => {
        app.use(core.expressMiddleware());
        app.listen(port, () => {
            console.log('server started at localhost:' + port);
        });
    }).catch(err => {
        console.log(err);
    });
// catch promise error
process.on('unhandledRejection', (err, promise) => {
    console.log('in unhandledRejection');
    console.log(err);
    // cannot redirect without ctx!
});文件中的所有项目文件路径(如 lavas-user/dist)以及启动端口号(如 8080)都可以根据项目实际情况进行修改。
文件的上半部分对 SPA 进行处理,核心是把 /user 开头的路由转发到 lavas-user 的入口 lavas-user/dist/index.html 上。其中还涉及到一个 URL 结尾 / 的小问题,我们在最后进行叙述。SPA 部分的最后是启动 express 服务器并监听端口,但因为 SSR 部分包含异步操作,因此 如果项目包含 SSR 部分,则这里可以注释,由 SSR 部分负责启动。
文件后半部分是对 SSR 进行处理,这部分和 lavas-main/server.prod.js 比较类似,就不再赘述了。
构建
- 分别对两个项目使用 
lavas build命令进行构建 - 复制任意一个项目的 
node_modules目录到根目录,供上述server.js使用 - 如果想精简项目内容,可以只将两个项目的 
dist目录移动出来,其余源码部分和node_modules都可以去除 (可选) 
最终目录结构
如果按照文档上列出的 server.js 中的配置,最终目录应该如下:
123456789101112131415lavas-project
├── lavas-user/
│   ├── dist
│   │   ├── index.html
│   │   └── something else (favicon.ico, lavas/, static/...)
│   └── something else (node_modules/, .lavas/, pages/, store/...)
│
├── lavas-main/
│   ├── dist
│   │   ├── server.prod.js
│   │   └── something else (lavas/, node_modules/, static/...)
│   └── something else (node_modules/, .lavas/, pages/, store/...)
│
├── node_modules/
└── server.js更进一步,精简后的代码结构可以是:
1234567891011lavas-project
├── lavas-user-dist/
│   ├── index.html
│   └── something else (favicon.ico, lavas/, static/...)
│
├── lavas-main-dist/
│   ├── server.prod.js
│   └── something else (lavas/, node_modules/, static/...)
│
├── node_modules/
└── server.js这应该是上线需求的最小集合了,为了适应这样的修改,还需要对 server.js 中的引用路径进行改动,这里就不重复了。
express 处理 SPA 路由的小问题 (扩展)
提示:这部分内容由 Lavas 内部处理,并不需要开发者进行参与,仅仅作为解答开发者疑问的扩展阅读存在。
在 server.js 中,我们能够发现存在一段代码:
12345678910// fix trailing slash (/user -> /user/)
app.use('/', function (req, res, next) {
    let requestUrl = req.url.replace(/\?.+?$/, '');
    if (requestUrl === url) {
        req.url = url + '/';
    }
    next();
});这段代码是用来处理一个 express 的路由问题的。Vue 官方推荐开发者在上线 Vue SPA 项目时使用 connect-history-api-fallback,这个中间件的核心是修改 express 的 req.url,让我们看看如下代码(截取自该中间件):
1234rewriteTarget = options.index || '/index.html';
logger('Rewriting', req.method, req.url, 'to', rewriteTarget);
req.url = rewriteTarget;
next();然后我们使用方式如下:
1234app.use('/user', historyMiddleware({
    htmlAcceptHeaders: ['text/html'],
    disableDotRule: false // ignore paths with dot inside
}))在这种配置下,当我们访问 /user/,经过中间件之后 req.url 会被设置为 /user/index.html,再进入 express.static,一切正常。但当访问 /user 时(没有后面那个 /),经过中间件之后会变成 /userindex.html,这样是无法被 express.static 识别的,当然落到 SSR 之后也无法匹配,因此会报出 404 错误。
因此我们在使用中间件之前还增加了一段修复代码,在访问 /user 的时候自动添加最后的 /。我们也考虑过 express 的 strict routing,但似乎也没法生效。如果开发者有更好的方法,欢迎告诉我们!
本文档系腾讯云开发者社区成员共同维护,如有问题请联系 cloudcommunity@tencent.com

