Ming YIN
Crazy Monster!
Ming Yin
@kalasoo
立个 Flag:这真的是我最最最最最最最最后一次作技术分享
我们有一个漫长的重构历史
2015年5-6月
单纯的组合业务,只有一个大的页面,全部放到一个独立的 Root Vue Object 里
2015年10月
2016年3-4月
2016年7-8月
2016年12月 - 2017年2月
掘金从每天十几个人访问
到每天十几万人访问
业务逻辑越来越复杂
但是 Vue.js 依然好用
❤️
直到今日,掘金的整体业务架构
prod
test
dev
build:client
build:server
deploy
juejin/
├── backend/ # 后端业务
├── build/ # build 脚本
├── cache/ # 页面 Redis 缓存
├── config/ # prod/dev build config 文件
├── src/ # Vue.js 核心业务
├── static/ # 静态文件
├── cdn.js # CDN 上传脚本
├── index.html # 最基础的网页
├── ...
├── renderer.js # SSR 脚本
└── server.js # Express 后端处理请求
{
...
"scripts": {
"start": "cross-env NODE_ENV=production node server",
"test": "cross-env NODE_ENV=production WRK_CNT=1 node server",
"stop": "pkill --signal SIGINT juejin-web",
"dev": "supervisor -q -n error -w build,config,backend server",
"build": "rimraf dist && npm run build:client && npm run build:server && npm run build:version",
"build:client": "cross-env NODE_ENV=production VUE_ENV=client webpack
--config build/webpack.client.conf.js --progress --hide-modules",
"build:server": "cross-env NODE_ENV=production VUE_ENV=server webpack
--config build/webpack.server.conf.js --progress --hide-modules",
"build:version": "cross-env NODE_ENV=production webpack
--config build/webpack.version.conf.js --progress --hide-modules",
"cdn": "node cdn",
"lint": "eslint --ext .js,.vue src"
},
...
}
package.json
Separate monolithic applications into multiple stand alone service applications (domain-subdomain) within different bounded contexts.
- ThoughtWorks
https://www.thoughtworks.com/insights/blog/domain-driven-design-services-architecture
Credit to Zhende @ xitu.io
src/
├── api/ # 接入微服务的基础 API
├── App/ # App Root Component
├── asset/ # 静态文件
├── business/ # 业务
├── component/ # 组件
├── const/ # 常量
├── event-bus/ # Event Bus 事件总线,类似 EventEmitter
├── global/ # 通用定义的 directive, mixin 还有绑定到 Vue.prototype 的函数
├── model/ # Model 抽象层
├── repository/ # 仓库,接入 Vuex 中
├── router/ # 路由
├── service/ # 服务
├── state/ # Vuex 状态管理
├── style/ # 样式
├── util/ # 通用 utility functions
├── view/ # 各个页面
├── client-entry.js # 前端业务 & build
├── server-entry.js # SSR业务 & build
├── ...
└── main.js # Vue Object Initiation
api/
util/
这个比较容易理解
service/
各个 Domain 下的基础功能业务
repository/
某一个独立 Domain 下的获取数据的业务
model/
数据抽象层
business/
各个 Domain 下的具体业务,会引用 service 和 repository 中定义的功能
validator/
不同数据的 validation 过程
state/ router/ component/ view/
Vue 下具体的交互展示层业务
类似于 Node 中的 EventEmitter
通过事件管理和监听处理异常、Alert、Scroll 触发等
一些不成熟的小经验
需求:
不通过跳转 URL 来显示 Not Found
解决方案:
{
path: '*',
name: 'notFound',
component: process.BROWSER
? () => System.import('view/NotFoundView')
: require('view/NotFoundView')
}
{
location: null,
errorView: null,
statusCode: 200
}
[SHOW_NOT_FOUND_VIEW] ({ commit }) {
commit(UPDATE_STATE, {
errorView: 'NotFoundView',
statusCode: 404
})
}
component.error-view(
v-if="errorView",
:is="errorView"
)
router-view(v-if="!errorView")
/entry/id-not-existed
需求:
后端渲染解决性能问题
解决方案:
多层缓存
function createRenderer (bundle) {
return vueServerRenderer.createBundleRenderer(bundle, {
cache: LRU({
max: 5000,
maxAge: 1000 * 60 * 60
}),
directives: {
link: require('./src/global/directive/link').server
}
})
}
renderStream.on('end', () => {
if (!shouldContinue) { return }
const initialState = encodeURIComponent(context.initialState)
const html = `
<script id="jjis">
window.__JJIS__="${initialState}"
</script>
${htmlTemplate[2]}
`
cachedHtml = cachedHtml + html
if (!user && context.code === 200) {
cache({
url: req.originalUrl,
html: cachedHtml
})
}
res.end(html)
})
需求:
前后端渲染数据一致性问题
解决方案:
通过某种事件广播机制实现数据的最终一致性
Vuex 本身就有事件广播模型,我们定义了 3 个 mutation 类型:
ENTITY_CREATED - 实体已创建
ENTITY_UPDATED - 实体已更新
ENTITY_DELETED - 实体已删除
import {
ENTITY_CREATED,
ENTITY_UPDATED,
ENTITY_DELETED
} from 'state/type'
let store = null
export function setup (tarStore) {
store = tarStore
}
export function emitEntityCreatedEvent (model, data) {
return store.commit(ENTITY_CREATED, { model, data })
}
export function emitEntityUpdatedEvent (model, data) {
return store.commit(ENTITY_UPDATED, { model, data })
}
export function emitEntityDeletedEvent (model, data) {
return store.commit(ENTITY_DELETED, { model, data })
}
import {
LIKE_ENTRY,
...
} from 'state/type'
import {
emitEntityUpdatedEvent,
emitEntityDeletedEvent
} from 'state/entity-event'
import * as entryService from 'service/entry'
import * as entryRepository from 'repository/entry'
const UPDATE_STATE = 'entry/UPDATE_STATE'
export default {
state: {},
mutations: {
[UPDATE_STATE] (state, newState) {
Object.assign(state, newState)
}
},
actions: {
[LIKE_ENTRY] ({ commit }, entry) {
return entryService.likeEntry(entry.id)
.then(() => {
entry.liked = true
entry.collectionCount += 1
})
},
...,
[ADD_ENTRY] ({ commit }, entry) {
return entryRepository.add(entry)
},
[UPDATE_ENTRY] ({ commit }, entry) {
return entryRepository.update(entry)
.then(() => emitEntityUpdatedEvent('Entry', entry))
},
[DELETE_ENTRY] ({ commit }, entryId) {
return entryRepository.remove(entryId)
.then(() => emitEntityDeletedEvent('Entry', { id: entryId }))
}
}
}
所有界面上对实体的变更操作都通过 dispatch action 来完成
this.$store.dispatch(DELETE_ENTRY, entryId)
// src/state/module/common/entry.js
[DELETE_ENTRY] ({ commit }, entryId) {
return entryRepository.remove(entryId)
.then(() => emitEntityDeletedEvent('Entry', { id: entryId }))
}
[ENTITY_DELETED] (state, info) {
if (shouldReact(info)) {
state.list = state.list.filter(item => item.id !== info.data.id)
}
}
我相信每一个新技术的出现的最终目的是创造价值
如果一个技术真的好
我就会推荐它,宣传它,帮助它普及给更多的开发者
---
这即是我做掘金的态度,也是我做技术的态度
By Ming YIN
掘金 Vue.js 2.0 后端渲染及重构实践