如何使用 Lavas 构建 MPA 项目

如何使用 Lavas 构建 MPA 项目

Lavas 目前支持两种渲染模式,分别是服务端渲染 (SSR) 和 浏览器端渲染。 而浏览器端渲染时,整个项目构建完成后只有一个 HTML 入口 index.html,因此这时本质上等价于单页应用,即 SPA。

从小型站点的开发需求来说,SPA 和 SSR 已经能够覆盖绝大部分。让我们更进一步,考虑如下两种情况:

  • 整个站点的一部分需要使用 SSR 模式渲染,另一部分使用 SPA 模式渲染。

如何使用 Lavas 来满足此类需求呢?

解决思路

我们可以把 Lavas 项目看做一个单独的单元,或称入口。

对于上述两种情况,都可以认为由多个入口组成:

  • 情况 1,相当于一些入口采用 SSR 模式,另一些入口采用 SPA 模式

因此,我们把问题归结为:多个 Lavas 项目如何整合到一起?

在 Lavas 构建完成后,SPA 项目本质上是一些静态页面,包含一个入口 index.html 和其他静态资源;SSR 项目本质上是一个 express (也可以是 koa )中间件。因此通过架设一个 express 服务器可以很方便分别对两者进行整合。

实现方式

为了表述方便,我们来创建一个简单但可能很实用的需求,通过解决这个需求来学习如何整合。

示例需求

我们假设开发者存在这样的开发需求:一个电商站点从业务模块角度能分成两部分,分别是:

  • /user/* 部分,用户信息浏览/注册/修改等等相关,采用 SPA 模式渲染。一般这类用户信息敏感的内容不需要考虑 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.js

Lavas 为开发者提供了 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 命令进行构建

最终目录结构

如果按照文档上列出的 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,但似乎也没法生效。如果开发者有更好的方法,欢迎告诉我们!