刘博文
极简主义
爱吹牛逼!
PHP渲染页面,在每个页面里引入组件使用
Nodejs + vue 前后端分离项目
Nodejs + vue 玩具项目
主题词后台管理系统
一个让你不吐不快,畅所欲言的主题社区
快言:http://k.hao.360.cn/
快言后台:http://topic.***.net/
.
├── README.md
├── build # build 脚本
├── config # prod/dev build config 文件
├── hera # 代码发布上线
├── index.html # 最基础的网页
├── package.json
├── src # Vue.js 核心业务
│ ├── App.vue # App Root Component
│ ├── api # 接入后端服务的基础 API
│ ├── assets # 静态文件
│ ├── components # 组件
│ ├── event-bus # Event Bus 事件总线,类似 EventEmitter
│ ├── main.js # Vue 入口文件
│ ├── router # 路由
│ ├── service # 服务
│ ├── store # Vuex 状态管理
│ ├── util # 通用 utility,directive, mixin 还有绑定到 Vue.prototype 的函数
│ └── view # 各个页面
├── static # DevServer 静态文件
└── test # 测试
纯前端工程
init
自动化初始化配置文件
dev
启动dev-server,hot-reload,http-proxy 等辅助开发
deploy
编译源码,静态文件上传cdn,生成html,发布上线
api /
请求数据,Mock数据,反向校验后端api
util /
存放项目全局的工具函数
service/
处理 server 返回的数据(类似data format)
例如 service 调用了不同的api接口拿到不同的数据整合在一起发送给 store
store /
vuex 的 store
router /
前端路由
view /
各个业务页面组件
component /
通用组件
event-bus /
主要用来处理特殊需求
创建完成
npm run init
config
├── dev.conf
│ └── index.js
├── init-tpl
│ ├── metadata.js
│ └── template
│ └── index.js
├── index.js
├── dev.env.js
├── prod.env.js
└── test.env.js
// init-tpl/template/index.js
module.exports = {
serverDomain: '{{serverDomain}}'
}
// init-tpl/metadata.js
module.exports = {
"prompts": {
"serverDomain": {
"type": "string",
"message": "Please enter the server address",
"default": "liubowen.topic.nav.qihoo.net"
}
}
}
// dev.conf/index.js
module.exports = {
serverDomain: 'liubowen.topic.nav.qihoo.net'
}
Speike-cli
// package.json
{
"scripts": {
"init": "speike init ./config/init-tpl ./config/dev.conf"
}
}
// .gitignore
config/dev.conf
import proxy from '../base.js'
import fetchLogs from './fetchLogs.js'
export default proxy.api({
fetchLogs
})
const actions = {
fetchLogs ({ commit }, query) {
return log.fetchLogs(query)
}
}
/api/log/index.js
/store/modules/log.js
/*
* /api/log/fetchLogs.js
*/
export default {
options: {
url: '/api/operatelog/list',
method: 'GET'
},
rule: {
'data': {
'list|0-20': [{
'id|3-7': '1',
'path': '/log/opreate',
'url': '/operate/log?id=3',
'user': 'berwin'
}],
'pageData|7-8': {
'cur': 1,
'first': 1,
'last': 1,
'total_pages|0-999999': 1,
'total_rows|0-999999': 1,
'size|0-999999': 1
}
},
'errno': 0,
'msg': '操作日志列表'
}
}
Mock 数据
反向校验后端api
可选,不强制使用
// 不使用api-proxy的api
import {api} from './base'
export default {
getUserInfo (sid) {
return api.get('/api/user/getUserInfo', {
params: {
sid,
host: window.location.hostname
}
})
}
}
npm run deploy
scripts: {
"deploy": "hera compile online"
}
npm run build
index.html
服务器
docker
一些不成熟的经验
和
曾经遇到过的问题
纯静态单页开发登陆认证问题怎么解决?
解决方案:
client
server
请求数据
返回未登录错误码
登陆中心
跳转
sid
请求数据(携带sid)
返回数据 + 种cookie
请求数据(携带cookie)
返回数据
在本地启动 Dev server 域名与端口号并不是后端域名,cookie怎么种?
解决方案:
client
server
请求数据(携带sid+host)
返回数据 + 种cookie
开发环境怎么搞?
.js
.vue
.css
images
dependencies
...
期望是编译出一个无任何依赖的纯html文件
HtmlWebpackPlugin
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Webpack App</title>
</head>
<body>
<script src="index_bundle.js"></script>
</body>
</html>
cdn-loader + cdn-plugin 自动上传 cdn
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: path.join(__dirname, 'cdn-loader'),
options: {
disable: !isProduction,
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
}
]
}
const CdnPlugin = require('./cdn-plugin')
plugins: [
new CdnPlugin()
]
cdn-loader 的作用是在编译过程中,代码内引入的图片,字体,等资源文件上传cdn
cdn-plugin 的作用是静态文件代码打包编译后,生成html文件的过程中,把编译好的 .js 与 .css 文件上传cdn并把上传cdn后得到的url写入html的 link 与 script 标签
var loaderUtils = require('loader-utils')
var qcdn = require('@q/qcdn')
module.exports = function(content) {
this.cacheable && this.cacheable()
var query = loaderUtils.getOptions(this) || {}
if (query.disable) {
var urlLoader = require('url-loader')
return urlLoader.call(this, content)
}
var callback = this.async()
var ext = loaderUtils.interpolateName(this, '[ext]', {content: content})
qcdn.content(content, ext)
.then(function upload(url) {
callback(null, 'module.exports = ' + JSON.stringify(url))
})
.catch(callback)
}
module.exports.raw = true
var qcdn = require('@q/qcdn')
function CdnAssetsHtmlWebpackPlugin (options) {}
CdnAssetsHtmlWebpackPlugin.prototype.apply = function (compiler) {
compiler.plugin('compilation', function(compilation) {
var extMap = {
script: {
ext: 'js',
src: 'src'
},
link: {
ext: 'css',
src: 'href'
},
}
compilation.plugin('html-webpack-plugin-alter-asset-tags', function(htmlPluginData, callback) {
console.log('> Static file uploading cdn...')
var bodys = htmlPluginData.body.map(upload('body'))
var heads = htmlPluginData.head.map(upload('head'))
function upload (type) {
return function (item, i) {
if (!extMap[item.tagName]) return Promise.resolve()
var source = compilation.assets[item.attributes[extMap[item.tagName].src].replace(/^(\/)*/g, '')].source()
return qcdn.content(source, extMap[item.tagName].ext)
.then(function qcdnDone(url) {
htmlPluginData[type][i].attributes[extMap[item.tagName].src] = url
return url
})
}
}
Promise.all(heads.concat(bodys))
.then(function (result) {
console.log('> Static file upload cdn done!')
callback(null, htmlPluginData)
})
.catch(callback)
})
})
}
module.exports = CdnAssetsHtmlWebpackPlugin
category: {
addImpl: true,
deleteImpl: true,
editImpl: true,
itemImpl: true,
getCategoryParentsImpl: true,
listImpl: true,
searchImpl: true
}
// or
category: false
const router = new Router({
mode: 'history',
routes: [
{
path: '/category',
name: 'category',
component: Category,
meta: {
permission: 'category'
}
}
]
})
const permission = {
category: {
addImpl: true,
deleteImpl: true,
editImpl: true,
itemImpl: true,
getCategoryParentsImpl: true,
listImpl: true,
searchImpl: true
}
}
router.beforeEach(function (to, from, next) {
if (permission[to.meta.permission] === false) {
router.push({name: 'error-authorize'})
return false
}
next()
})
使用v-if控制是否显示
server端会对api权限做验证,所以前端没做权限认证,直接走全局错误处理逻辑
axios.interceptors.response.use(function (response) {
if (response.data.errno !== 0) {
Bus.$emit('message', {
message: response.data.msg || '服务器发生错误',
type: 'error'
})
return Promise.reject(response)
}
return response
}, function (error) {
return Promise.reject(error)
})
<el-button
type="danger"
size="mini"
@click="remove(scope.row.id)"
v-if="permission.category.deleteImpl">
删除
</el-button>
computed: {
...mapState({
permission: state => state.user.permission
})
}
handleCurrentChange (val) {
this.$router.push({
name: 'word',
query: {
...this.$route.query,
page: val
}
})
}
search () {
this.$router.push({
name: 'word',
query: {
search: this.searchKeyword,
cate_id: this.cateId
}
})
}
watch: { '$route' () { this.refreshList() } }
refreshList () {
return this.changeLoadingState(true)
.then(this.fetchList)
.then(this.updateStore)
.then(this.changeLoadingState.bind(this, false))
.catch(this.changeLoadingState.bind(this, false))
}
fetchList () {
const query = this.$route.query
if (query.search) return this.search()
if (query.cate_id) return this.searchByCid()
return this.fetchWords()
}
// src/store/modules/category.js
const actions = {
refreshCategories ({ commit }, query) {
return category.getCategories(query).then(({data}) => {
commit(types.RECEIVE_CATEGORIES, { categories: data.data.list })
commit(types.RECEIVE_CATEGORY_PAGEDATA, { pageData: data.data.pageData })
})
}
}
// src/view/Category/List.vue
this.Categories({
pageno: this.currentPage,
size: this.currentSize,
pid: this.$route.query.parentIds
})
第一页
Server
请求第一页列表
响应第一页列表 并 更新store中的列表和页码数据
第二页
请求第二页列表
响应第二页列表 并 更新store中的列表和页码数据
第三页
请求第三页列表
响应第三页列表 并 更新store中的列表和页码数据
第一页
Server
请求第一页列表
响应第一页列表 并 更新store中的列表和页码数据
第二页
请求第二页列表
响应第二页列表 并 更新store中的列表和页码数据
第三页
请求第三页列表
响应第三页列表 并 更新store中的列表和页码数据
第一页
第二页
第三页
跳回第一页
跳回第二页
跳回第三页
// Panel.vue '$route.query.id' () { this.search() }
// List.vue '$route.query.page' () { this.refresh() }
有些路要自己走,
有些坑要自己踩
我无法通过一个PPT把所有经验都讲出来
By 刘博文
Vue 项目架构设计与工程化实践