Vue SPA + Nodejs项目实战
文章链接:blog.csdn.net/i348018533/…
本文主要阐述的是使用了vue spa单页应用的后台管理系统配合nodejs接口代理,并使用webpack打包编译的项目架构分享。
面向对象是有一定
前端开发基础对vue nodejs webpack有一定认识的开发。
Vue简介
介绍
- 自底向上增量开发的设计,易上手同时便于与其他现有系统整合
- 组件化应用构建
- 同样丰富的生态也可以实现复杂应用程序
- 数据驱动视图
我觉得以上四点就可以概括的介绍vue的定义,因为Vue 的核心库只关注视图层所以在任何现有项目中你可以非常方便的以引入的形式把vue添加进来,不会对现有系统造成任何的侵入,同时因为vue也拥有非常丰富的生态环境(vue-router、vuex、weex、ssr)所以使用vue也可以搭建中大型的系统。
vue 2的一些重大改进
- Virtual-DOM
- 兼容的Templates, JSX
- SSR
这里需要提及的是SSR,目前SSR官方文件只有英文版本,官方推荐的是使用 NUXT框架即可轻松实现SSR。
如果想要自己实现SSR的话也不复杂,主要的难点在于如何编写一套既可在在客户端运行又可以在服务端运行的通用代码,比如在代码中使用window.location、document.getxxx等等这些对象只存在于浏览器环境中服务端nodejs是不存在的,并且服务端代码中vue实例化后是不能渲染的,服务端只是通过虚拟dom解析为html文本的,所以在服务端也不能和客户端一样实例化后就挂载到dom上去。
解决的方法就是在webpack中定义两套入口文件,只能在客户端运行的代码在客户端入口中加载,服务端入口也是独立的,然后通过vue官方提供的VueSSRServerPlugin和VueSSRClientPlugin两个webpack插件把两份代码打包为bundle包。
关于SSR的性能的vue的状态管理不是immutabled的,另外因为虚拟dom到html的编译过程导致ssr的性能一直无法提升上来,但是也可以通过一些缓存的策略弥补,如果对于首屏时间要求苛刻的项目可能暂时无法接受。
vue的更多信息请访问: cn.vuejs.org
项目结构
后端
后端的结构非常轻量因为它的主要作用就是充当一个接口代理和权限验证的作用,所以通过几个简单的中间件来处理即可:
请求->before: 处理用户请求的数据做数据整合
before->controllers: 一些登录和非业务逻辑操作
controllers->auth: 在调用后端接口之前的鉴权逻辑
auth->proxy: 鉴权通过则允许调用后端接口通过request的pipe直接把后端返回流返回到前端
catch: 捕获错误
前端
这也是一张非常典型的vue spa架构的结构图,使用vue-router来处理路由,axios来处理异步数据请求,同时项目使用iview作为ui框架(吐槽:目前iview仍是2.x rc版本,对于jsx和ssr支持度不高) 因为lodash实在太好用了,既然是后端项目应该也可以忍受多出的几十kb的文件。
前端的目录结构如下:
解释:
- components 公共组件,注册到全局实例中:
- config 项目的配置文件
- directives 项目中使用到的自定义指令
- filters 项目中使用到的自定义过滤器
- pages 业务目录,以业务模块功能划分,页面相关的组件和store存在放对应的页面文件夹内
- plugins 系统插件,把路由、cache、auth等功能封装为一个插件在vue初始化的时候安装到实例中
- services 数据处理、异步数据请求
- statics 静态资源文件(fonts、image等)
- util 帮助类
组件化
设计原则:
- 可复用
- 抽象分而治之
- 黑箱
如果一个功能是会被多个地方使用那么它一定可以被做成组件,一个组件需要负责哪些业务它的功能有哪些,这些在定义组件之前就需要明确,否则会越来越模糊。
一些不可复用组件也需要被抽象出来,因为页面被组件化拆分后可以有效降低页面代码的复杂度,一个形象的例子是:一个1000行的代码文件和10个每个只有100行代码的文件,当复杂功能被一层层抽象拆分后整体复杂度会慢慢降低。
组件对于使用者来说是一个黑箱状态,使用者需要知道组件需要传入什么数据、组件有哪些功能、组件会输出哪些数据即可,对于组件内部的实现逻辑使用者不必关心。
下面可以看带一个页面组件划分:
不同页面的框代表他们的层级。
路由管理
history mode
项目使用的是官方vue-router库,可以很简单的实现嵌套、同级路由,并且支持history mode。 默认路由是hash mode,既用户访问的路径为以下形式
/#/home.html
而history mode就和普通url无异了:
/home.html
history mode是需要web服务器提供支持的,当访问/home.html路径时web服务器因为找不到这个路径而返回404,这时候在web服务器中的所有页面请求都应该把入口文件输出。
nginx:
location / {
try_files $uri $uri/ /index.html;
}
location ~ \.html$ {
try_files $uri $uri/ /index.html;
}
nodejs:
let mainProxy = (req, res) => {
res.header('x-version', env.version);
res.header('cache-control', 'no-store');
rp.get(`http://cdn.xxx.cn/${env.version}/index.html`).then(body => {
res.send(body);
});
};
router.get('/', mainProxy);
router.get(/\.html$/, mainProxy);
路径定义路由
Vue的路由配置非常简单:
const User = {
template: '<div>User</div>'
}
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头
{ path: '/user/:id', component: User }
]
})
只需要在每个路由的path定义路径即可,而且因为系统中pages的目录层级较多也会比较深,如果每个页面单独定义path的会很麻烦且不易维护,所以在项目的pages中每个文件夹包括子文件夹都提供了一个入口文件,这个入口文件负责把每个路由整合到一起合并为一个路由树对象,最后把这个路由数对象扁平化处理,就得到一个拥有各自层级的path路径了:
路由定义,可以支持三种形式:
最简单的导出一个路由
export default {
path: '/overview.html',
name: 'overview',
component: () => import(/* webpackChunkName: "home.overview" */'./overview'),
};
自定义层级
export default {
list: {
path: '/onsale.html',
name: 'onsale',
component: () => import(/* webpackChunkName: "product.onsale" */'./onsale'),
meta: {
pageName: '在售商品'
}
}
};
以及数组
export default [{
path: '/list.html',
name: 'list',
component: () => import(/* webpackChunkName: "repository.express" */'./list'),
meta: {
pageName: '入库物流管理'
}
}, {
path: '/:id.html',
name: 'info',
component: () => import(/* webpackChunkName: "repository.express" */'./info'),
meta: {
pageName: '入库物流详情'
}
}];
入口文件:
import overview from './overview';
export default {
overview
};
开发构建
Hot Module Replacement
使用vue提供的脚手架默认就是热更新的所以这里不多赘述
DllPlugin
dll plugin是webpack2功能,我们系统一般更新发布版本时会通过webpack打包生成的文件中带有hashcode来实现文件的强缓存,但是一些几乎不会更新的包比如:vue、vue-router、lodash、iview等,在每次发布版本时的hashcode变化导致客户端每次升级都需要下载一次这些包,造成一些不必要的浪费,而且在webpack打包时每次build也都会把这些几乎不会更新的包重新编译一次又是一些不必要的浪费,所以dll plugin就出现了,现在我们可以把这些不会更新的包单独打包并且生成一个清单文件告诉webpack:“这些文件是现成的,如果你编译的时候遇到有引用的这些项目麻烦你跳过去!”,这样webpack在线下构建速度提高了,在客户端每次升级是因为dll的hashcode不会改变,利用浏览器缓存,那客户端每次更新也只会更新实际需要更新的文件了。
dll的构建和平常webpack打包的构建是一样的只不过需要特别引用一个DllPlugin插件:
{
output: {
path: path.join(__dirname, '/dll'),
filename: '[name].[chunkhash:7].js',
library: '[name]_[chunkhash]'
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
entry: {
vendor: [
'vue',
'lodash',
'axios',
'vue-router',
'iview',
'moment',
'iview/dist/styles/iview.css'
],
},
resolve: {
alias: {
vue$: 'vue/dist/vue.esm.js'
}
},
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true
})
},
plugins: [
new webpack.DllPlugin({ // DllPlguin插件,会生成一个manifeat.json清单
path: path.join(__dirname, '/dll/manifest.json'),
name: '[name]_[chunkhash]',
context: __dirname,
}),
new UglifyJsparallelPlugin({
workers: os.cpus().length,
mangle: true,
compress: {
warnings: false
},
comments: false,
sourceMap: true
}),
new AssetsPlugin({ //把编译结果文件生成一个json,在引用dll时用到
filename: 'bundle-config.json',
prettyPrint: true,
fullPath: false,
path: path.join(__dirname, '/dll/')
}),
new ExtractTextPlugin({
filename: '[name].[contenthash:7].css'
})
],
}
这里需要提及的是,DllPlugin不会生成结果文件的清单,所以在引用dll时需要手动引入dll生成的资源文件的,但是我们在dll生成文件中加入了hashcode,导致每次引用的名称可能不同,所以我们利用AssetsPlugin插件单独生成一份打包后的文件清单。
在项目webpack配置文件中利用DllReferencePlugin插件告诉webpack哪些是不需要打包的,然后利用AddAssetHtmlPlugin把dll的结果文件添加到index.html中即可。
let assets = _.map(bundleConfig.vendor, (asset, name) => {
return {
filepath: require.resolve(`./dll/${asset}`),
outputPath: utils.assetsPath(name),
typeOfAsset: name
}
})
plugins: [
new AddAssetHtmlPlugin(assets),
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./dll/manifest.json'),
})
]
Lazy Loading
vue-router是支持异步加载视图的而且实现起来非常简单:
import在webpack 2.x已经支持了,因为这是一个es提案目前之态stage-2中支持,如果需要单独只是的话需要在babel配置中添加syntax-dynamic-import插件,如果使用了eslint来检测语法的话也需要在eslint的配置中把parser修改为babel-eslint。
import中的webpackChunkName是声明异步包名称的语法,这也是webpack2.x支持的magic
comment
export default {
path: '/overview.html',
name: 'overview',
component: () => import(/* webpackChunkName: "home.overview" */'./overview'),
};
Code Generation
因为是管理类项目,页面的结构大多都是类似的,所以在每次开发时新建页面很多工作是重复的,比如定义页面vue模板内容、注册路由等,所以在项目中我们自己写了一套很简单的代码生成工具,简单且有用。
npm run code -module=testmodule -page=testmodule -mode=lsit
通过以上命令就可以自动生成一个列表模板页面并且注册好了路由,通过opn组件自动打开浏览器定位到新建的页面。
在模板文件夹中可以定义很多模板文件,比如页面示例文件、页面路由等等,通过一些文件内容中的占位符来替换在run命令时输入的module和page名称。
至于如何在已有的入口文件中把新建的模块添加进去,我们则利用了acorn来解析入口文件变成一个AST树,在树结构中添加新建模块的结构后,再利用escodegen把AST树解析为字符代码输出到入口文件中。
入口文件:
处理后:
抛出的几个问题
组件独立调试和单元测试
单独对组件进行调试不需要测试组件时必须要打开组件所在的页面
组件的单元测试
以上是目前还没有接触到的未来计划会慢慢添加到项目中。
DllPlugin打包出的图片和字体文件
对于dll打包后如果有图片、字体等静态文件目前可行的办法只有在项目编译时把这些资源拷贝过去,暂时没有什么好的办法
webpack 的Tree shaking鸡肋?
实际实验之后发现真的如webpack官网所述的在文件的根节点export多个情况下才能被webpack解析到,比如:
// This function isn't used anywhere
export function square(x) {
return x * x;
}
// This function gets included
export function cube(x) {
return x * x * x;
}
这种情况下使用其中一个函数webpack打包会自动剔除其他的代码,但是这种情况好像真的不多啊。
以上就是一次vue项目的架构分享总结,之前只开发过angular所以对于双向绑定组件等概念有了一定认识,接触vue会更轻松些,但是vue本身的学习曲线真的很低,并且提供了结构清晰的中文文档,总结一句话:vue简单、好用