今年的一个项目要用到lin-cms-vue框架,长话短说,就是在lin-cms-vue的基础上封装了路由、接口请求、axios的拦截、一些处理函数、一些公共的框架(如lin-table)。当然,已经有成熟轮子的用户完全用不到。我也是抱着试一下的心态看看。之间也接触过一些好的框架,包括一些大公司牛人自己封装一些框架。
还是啰嗦了,其实lin-cms-vue就是对vue进行了二开,方便后续的开发。
官网地址:https://doc.cms.talelin.com/client/
项目clone下来后运行。
因为后端项目lin-cms-koa调用了插件’@koa/cors’,所以不用配置代理,直接和后端联调就可以了。
现在进入正题说一下lin-cms-vue的开发心得:

一、组件封装

1、table

对于一个管理后台类目框架,最重要的莫过于对el-table的封装,然而,lin-table封装的不够好,并不尽如人意。

槽点一:

首先吐槽操作栏operate,这种方式需要将operate中定义的方法在重新定义一次,否则会找不到这个方法,弃用了(但是代码中保留了operate方法),最后直接render的。
render方法如下:

  1. <el-table-column
  2. v-if="operate && operate.length"
  3. label="操作"
  4. fixed="right"
  5. class="action-btn-wrap"
  6. :width="actionWidth"
  7. >
  8. <template slot-scope="scope">
  9. <template v-for="(item, index) in operate">
  10. <el-button
  11. v-if="item.key ? testBtn(scope.row, item.key, item.value) : true"
  12. :type="item.type"
  13. plain
  14. :key="index"
  15. size="mini"
  16. v-permission="{ permission: item.permission ? item.permission : '', type: 'disabled' }"
  17. @click.native.prevent.stop="buttonMethods(item.func, scope.$index, scope.row)"
  18. >
  19. {{ item.name }}
  20. </el-button>
  21. </template>
  22. </template>
  23. </el-table-column>
  24. // 就直接render吧
  25. <el-table-column
  26. v-else-if="useActionRender"
  27. label="操作"
  28. fixed="right"
  29. class="action-btn-wrap"
  30. :width="actionWidth"
  31. >
  32. <template slot-scope="scope">
  33. <table-column
  34. :index="scope.$index"
  35. :row="scope.row"
  36. :col="scope.column"
  37. :render="actionRender" />
  38. </template>
  39. </el-table-column>

然后组件中需要渲染的地址直接jsx语法:

  1. <!-- 表格 -->
  2. <lin-table
  3. title="工单列表"
  4. :tableColumn="tableColumn"
  5. :tableData="tableData"
  6. :useActionRender="true"
  7. :action-render="actionRender"
  8. :showSelectCol="true"
  9. :index="true"
  10. :actionWidth="150"
  11. :pagination="pagination"
  12. :currentChange="currentChange"
  13. :sizeChange="sizeChange"
  14. v-loading="loading"
  15. ></lin-table>
  16. // 方法写在了methods中:
  17. actionRender(h, { index, row, col }) {
  18. const authority = this.user.role === 0 || this.user.role === 1 ? true : false
  19. return (
  20. <div class="action-wrap">
  21. {/*客服登录才有编辑按钮*/}
  22. <el-button v-permission={['客服']} size="mini" plain type="primary" onClick={() => this.handleEdit(index, row, col)}>
  23. 编辑
  24. </el-button>
  25. <el-button size="mini" plain type="primary" onClick={() => this.goToGroupEditPage(index, row, col)}>
  26. 详情
  27. </el-button>
  28. </div>
  29. )
  30. }

槽点二:

对于table来说,最重要的就是搜索栏了。但是很遗憾没有配套的,只煎蛋的lin-search之类的。不知道这样的封装意义何在?
看一下框架自带的代码:
image.png
无用的很。

自己做了seach的封装。可以用mixin方法将search、reset都封装为公共的,这样就不用每个table页面都单独search。最后因为实际项目的原因,只将reset方法封装在了mixin中。

  1. <template>
  2. <div class="basic-search">
  3. <div class="basic-search__hd" v-show="showHd">
  4. <span class="basic-search__hd-line"></span>
  5. <span class="basic-search__hd-title">筛选条件</span>
  6. </div>
  7. <el-row class="basic-search__bd">
  8. <el-col class="basic-search__col-l" :span="leftSpan">
  9. <el-form
  10. ref="form"
  11. :model="query"
  12. :label-position="labelPosition"
  13. :label-width="labelWidth + 'px'"
  14. @submit.native.prevent
  15. :size="size"
  16. >
  17. <slot>&nbsp;</slot>
  18. </el-form>
  19. </el-col>
  20. <el-col class="basic-search__col-r" :span="24 - leftSpan">
  21. <template v-if="mode === 2">
  22. <el-row>
  23. <el-col :span="24">
  24. <el-button type="primary" icon="el-icon-search" @click="handleSearch(query)">查询</el-button>
  25. </el-col>
  26. </el-row>
  27. <el-row>
  28. <el-col :span="24">
  29. <el-button icon="el-icon-refresh" @click="handleReset" v-if="showReset">重置</el-button>
  30. </el-col>
  31. </el-row>
  32. </template>
  33. <template v-else>
  34. <el-button type="primary" icon="el-icon-search" @click="handleSearch(query)">查询</el-button>
  35. <span style="margin:0 2px;">&nbsp;</span>
  36. <el-button icon="el-icon-refresh" @click="handleReset" v-if="showReset">重置</el-button>
  37. </template>
  38. <a
  39. title="展开更多"
  40. class="toggle-more-btn"
  41. :class="[closed ? 'open' : 'close']"
  42. @click="toggleMore"
  43. v-if="showMore"
  44. ></a>
  45. </el-col>
  46. </el-row>
  47. </div>
  48. </template>
  49. <script>
  50. export default {
  51. props: {
  52. showHd: {
  53. type: Boolean,
  54. default: true
  55. },
  56. labelPosition: {
  57. type: String,
  58. default: 'right',
  59. },
  60. labelWidth: {
  61. type: Number,
  62. default: 80,
  63. },
  64. // 模式(1: 单行, 2: 多行)
  65. mode: {
  66. type: Number,
  67. default: 2,
  68. },
  69. showMore: {
  70. type: Boolean,
  71. default: false,
  72. },
  73. showReset: {
  74. type: Boolean,
  75. default: true,
  76. },
  77. closed: {
  78. type: Boolean,
  79. default: true,
  80. },
  81. query: {
  82. type: Object,
  83. default() {
  84. return {}
  85. },
  86. },
  87. toggleMore: {
  88. type: Function,
  89. default: () => {},
  90. },
  91. handleSearch: {
  92. type: Function,
  93. default: () => {},
  94. },
  95. handleReset: {
  96. type: Function,
  97. default: () => {},
  98. },
  99. size: {
  100. type: String,
  101. default: 'small',
  102. },
  103. },
  104. computed: {
  105. leftSpan() {
  106. if (this.showReset) {
  107. return this.mode === 2 ? 21 : 18
  108. }
  109. return 21
  110. },
  111. },
  112. }
  113. </script>
  114. <style lang="scss" scoped>
  115. /deep/.el-select {
  116. width: 100%;
  117. }
  118. .basic-search {
  119. border-bottom: 1px solid #e2e2e5;
  120. padding: 20px 30px;
  121. }
  122. .basic-search__hd {
  123. margin-bottom: 24px;
  124. }
  125. .basic-search__hd-line {
  126. display: inline-block;
  127. margin-right: 10px;
  128. width: 4px;
  129. height: 16px;
  130. border-radius: 4px;
  131. vertical-align: middle;
  132. background: #4186f6;
  133. }
  134. .basic-search__hd-title {
  135. color: #3f4656;
  136. font-size: 14px;
  137. }
  138. .basic-search__bd {
  139. padding: 0 20px 0 15px;
  140. }
  141. .basic-search__col-l {
  142. padding-right: 16px;
  143. -webkit-user-select: none;
  144. -moz-user-select: none;
  145. -ms-user-select: none;
  146. user-select: none;
  147. }
  148. .basic-search__col-r {
  149. position: relative;
  150. /deep/ .el-row:first-child {
  151. margin-bottom: 22px;
  152. }
  153. .toggle-more-btn {
  154. position: absolute;
  155. top: 50%;
  156. right: -18px;
  157. width: 14px;
  158. height: 14px;
  159. margin-top: -8px;
  160. background: no-repeat center center;
  161. background-size: contain;
  162. &.open {
  163. background-image: url('~@/assets/image/icon-open.png');
  164. }
  165. &.close {
  166. background-image: url('~@/assets/image/icon-close.png');
  167. }
  168. }
  169. }
  170. /deep/.el-form-item__label {
  171. text-align: left;
  172. }
  173. </style>

然后mix中处理:

  1. import Utils from 'lin/util/util'
  2. import BasicSearch from '@/component/base/basic-search'
  3. /**
  4. * 列表页查询表单公共方法混入
  5. */
  6. export default {
  7. components: {
  8. BasicSearch,
  9. },
  10. props: {
  11. updateState: Function,
  12. handleSearch: Function,
  13. },
  14. data() {
  15. return {
  16. query: this.getDefaultQuery(),
  17. closed: true,
  18. }
  19. },
  20. methods: {
  21. toggleMore() {
  22. this.closed = !this.closed
  23. },
  24. handleReset() {
  25. this.query = this.getDefaultQuery()
  26. this.handleSearch(this.query)
  27. },
  28. getDefaultQuery() {
  29. const { fields } = this
  30. const query = {}
  31. for (let i = 0; i < fields.length; i++) {
  32. if (Utils.isObject(fields[i])) {
  33. query[fields[i].key] = fields[i].default
  34. } else {
  35. query[fields[i]] = undefined
  36. }
  37. }
  38. return query
  39. },
  40. },
  41. }

最后在每个模块中单独一个search文件:

  1. <template>
  2. <basic-search
  3. :label-width="90"
  4. :query="query"
  5. :show-more="true"
  6. :closed="closed"
  7. :mode="1"
  8. :toggle-more="toggleMore"
  9. :handle-search="handleSearch"
  10. :handle-reset="handleReset"
  11. >
  12. <el-row :gutter="24">
  13. </el-row>
  14. <div class="more-row-wrap" v-show="!closed">
  15. </div>
  16. </basic-search>
  17. </template>
  18. <script>
  19. import ExactSearch from '@/lin/mixin/exact-search'
  20. import { customerSourceData } from 'lin/format/replace-sheet'
  21. export default {
  22. mixins: [ExactSearch],
  23. components: {},
  24. props: {},
  25. data() {
  26. return {}
  27. },
  28. watch: {
  29. 'query.dateRange':function(val) {
  30. if(val && val.length === 2) {
  31. this.query.start_time = val[0]
  32. this.query.end_time = val[1]
  33. } else {
  34. this.query.start_time = undefined
  35. this.query.end_time = undefined
  36. }
  37. }
  38. },
  39. beforeCreate() {
  40. this.fields = ['phone', 'source', 'name', 'client_type', 'dateRange']
  41. },
  42. }
  43. </script>

然后在table页面引入search组件:

  1. import ExactSearch from './components/exactSearch'
  2. import LinTable from '@/component/base/table/lin-table'
  3. export default {
  4. name: 'CustomerMg',
  5. components: {
  6. LinTable,
  7. ExactSearch,
  8. },

最后呈现效果如下图:
微信图片_20201116145439.png

二、说一下亮点吧

1、api的封装

每个模块api都是一个构造函数,鄙人之前习惯于将每个模块api写在同一个js文件中,但是每一个接口是一个函数export出去,这样方便组件单独引入每一个接口。当然也可以通过import * as 来将所有的函数作为一个对象暴露出来。也很方便。

2、关于content部分是放在keep-alive中呢,还是不放呢,众说纷纭。本框架显然是没有放的。

三、打包部署和本地ngix来检查你的打包

1、配置publicPath

因为项目不是在域名的根目录下,而是子目录/service下,所以有三处地方做了更改:

  • vue.config.js中的publicPath
  • router中index.js在router实例下添加了base
  • 因为config中的sade-bar配置都是请求的public中的图片,特意在gutter.js下做了处理

说明一下,因为接口请求的时候服务器也没有nginx代理(通过标识),我都是绝对路径,所以没有必要配置/service。

三处截图如下:

image.png

image.png

image.png

我一直觉得这种处理方式不够优雅,我觉得可以服务器在nginx做配置,我这里不需要做处理了。不过既然服务器没有做nginx配置,那只能我这里处理了。

2、本地nginx检查dist包

为了校验build包是否再服务器可用,我在本地下载了一个适合window版本的nginx。
下载地址:http://nginx.org/en/download.html
我选择的还是1.16.1版本
image.png

通过cmd启动nginx,之后配置如下:
我的打包后的文件放在了d盘额Service/service文件夹下:

nginx.conf文件如下:

  1. #user nobody;
  2. worker_processes 1;
  3. #error_log logs/error.log;
  4. #error_log logs/error.log notice;
  5. #error_log logs/error.log info;
  6. #pid logs/nginx.pid;
  7. events {
  8. worker_connections 1024;
  9. }
  10. http {
  11. include mime.types;
  12. default_type application/octet-stream;
  13. #log_format main '$remote_addr - $remote_user [$time_local] "$request" '
  14. # '$status $body_bytes_sent "$http_referer" '
  15. # '"$http_user_agent" "$http_x_forwarded_for"';
  16. #access_log logs/access.log main;
  17. sendfile on;
  18. #tcp_nopush on;
  19. #keepalive_timeout 0;
  20. keepalive_timeout 65;
  21. #gzip on;
  22. # 系统
  23. server {
  24. listen 3000;
  25. server_name localhost;
  26. #charset koi8-r;
  27. #access_log logs/host.access.log main;
  28. root D:/Service/;
  29. ### 因为模拟了生产环境就是****.cn/service 的子目录
  30. location /service{
  31. index index.html;
  32. ### 采用的hsitory模式,此处配置了,当找不到文件路径重定向到index.html
  33. try_files $uri $uri/ /service/index.html;
  34. }
  35. location / {
  36. ### 此处是项目部署的域名
  37. proxy_pass http://*******.cn;
  38. proxy_set_header Host ********.cn;
  39. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  40. }
  41. error_page 500 502 503 504 /50x.html;
  42. location = /50x.html {
  43. root html;
  44. }
  45. }
  46. }

至此,大功完成,nginx -s reload 后,输入,本地路径http://localhost:3000/service/index.html, 成功进入页面。