当前位置:   article > 正文

vue 快速入门 系列 —— 玩转 CMS

vue 快速入门 系列 —— 玩转 CMS

玩转 CMS

目前接手的内容管理系统(CMS)基于 ant-design-vue-pro(简称模板项目ant-vue-pro) 开发的,经过许多次迭代,形成了现在的模样(简称本地项目)。

假如让一名新手接手这个项目,他会遇到很多问题,比如 .env 的作用、开发时后端接口没有写好如何联调、样式使用less还是 CSS Modules、表单和表格如何使用等等

技术是为产品服务,只需要能用技术做出项目,不需要所有技术、所有最佳实践都清楚。好比中医发展了好几千年,许多本源的东西老中医也是不清楚的,但我们摸索出一套规则,按照这个能治病,这个就很好。

本地项目使用的是 ant design vue 1.x 版本,基于 vue 2

样式

Ant Design Pro 默认使用 less 作为样式语言。

Tip: less 语法 —— 重要,不紧急(后续补上)。直接在 less 中用 css 语法也能完成项目,然后逐步的利用 less 功能。

vscode 搜索,发现 90% 以上都有 scoped,样式语言也确实是 less。

  1. // 69
  2. <style
  3. // 49
  4. <style lang="less" scoped>
  5. // 15
  6. <style scoped>

样式开发过程,要避免全局污染,通过 scoped 特性和 css modules 设置组件样式作用域。

  1. <style lang="less" scoped>
  2. .chart-trend {
  3. display: inline-block;
  4. font-size: 16px;
  5. line-height: 24px;
  6. }
  7. </style>

Tip:

  • 有关 scoped 更多介绍请看这里
  • 如何在 Vue 中优雅地使用 CSS Modules,可以参考网友的 文章

:避免在 scoped 中使用元素选择器。比如转成 button[data-v-xxxxxx] 会比类选择器组合要慢,因为要匹配的元素太多了。

@import

在单文件组件的样式中,通过 @import 引入 less 文件:

  1. <template>
  2. <div>
  3. <p class="red">hello</p>
  4. </div>
  5. </template>
  6. <style lang="less" scoped>
  7. @import './index.less';
  8. </style>
  1. // index.less
  2. .red{
  3. color: red;
  4. font-size: 23.5px;
  5. }

请问 .red 是全局的还是局部的,是否会影响到其他页面?

笔者测试发现,是局部的。最后编译出来是这样:

  1. <style type="text/css">.red[data-v-2b80bebf] {
  2. color: red;
  3. font-size: 23.5px;
  4. }
  5. </style>

Tip:网上有的说这么写是全局。

如果不加 scoped 则会全局生效。就像这样:

  1. <style lang="less">
  2. @import './index.less';
  3. </style>
导入写法
  • 从 ant-design-vue 库的样式文件中导入 index.less 文件
  1. // index.less
  2. // ~ant-design-vue/lib/style/index 表示从 ant-design-vue 库的样式文件中导入 index 文件
  3. // 导入的是 index.less 文件,而不是 index.css
  4. //Less 中,通过 @import 关键字导入的文件可以是 Less 文件或 CSS 文件。如果导入的文件没有指定后缀名,Less 会尝试导入同名的 .less 文件,如果不存在,Less 会尝试导入同名的 .css 文件。
  5. @import '~ant-design-vue/lib/style/index';
  • 同一目录下的名为chart.less的文件
  1. <style lang="less" scoped>
  2. // 同一目录下的名为chart.less的文件。不存在,Less 将会继续尝试导入同名的chart.css文件
  3. @import "chart";
  4. </style>
  • @import '../index.less'; 是 Less 的新语法格式,它不使用 url() 函数。更加简洁和直观。
  1. // 旧语法
  2. @import url('../index.less')
  3. // 新语法
  4. @import '../index.less';

使用的是支持新语法的 Less 版本,这两种写法是等价的。

  • @import "~@/components/index.less"; 是一种在 Less 中导入模块化组件的常见方式。
  1. <style lang="less" scoped>
  2. // 正确
  3. @import "~@/components/test.less";
  4. // 错误。less 并不会识别 @ 符号作为项目根目录的表示
  5. // @import "@/components/test.less";
  6. </style>
样式文件类别

在一个项目中,样式文件根据功能不同,可以划分为不同的类别

公共样式

可以将样式提取到一个公共文件,比如 Pro 提取的 src/global.less 然后在 main.js 将样式引入 import './global.less'

工具样式

src/utils/utils.less 这里可以放置一些工具函数供调用,比如清除浮动 .clearfix。

  1. // mixins for clearfix
  2. // ------------------------
  3. .clearfix() {
  4. zoom: 1;
  5. &::before,
  6. &::after {
  7. display: table;
  8. content: ' ';
  9. }
  10. &::after {
  11. height: 0;
  12. clear: both;
  13. font-size: 0;
  14. visibility: hidden;
  15. }
  16. }

.clearfix() 混合器定义了清除浮动的样式。然后,你可以通过 .clearfix() 在选择器 .selector 中调用混合器,从而应用清除浮动的样式:

  1. .selector {
  2. // 调用 .clearfix() 混合器
  3. .clearfix();
  4. }
通用模块级

例如 src/layouts/BasicLayout.less,里面包含一些基本布局的样式,被 src/layouts/BasicLayout.vue 引用,项目中使用这种布局的页面就不需要再关心整体布局的设置。如果你的项目中需要使用其他布局,也建议将布局相关的 js 和 less 放在这里 src/layouts

组件级

组件相关的样式,有一些在页面中重复使用的片段或相对独立的功能,你可以提炼成组件,相关的样式也应该提炼出来放在组件中,而不是混淆在页面里。

Tip:有时样式配置特别简单,也没有重复使用,你也可以用内联样式 style="{ fontSize: fontSizeVar }" 来设置。

覆盖组件样式

由于业务的个性化需求,我们经常会遇到需要覆盖组件样式的情况。请看示例:

  1. <template>
  2. <div class="test-wrapper">
  3. <a-select v-model="name" style="width:400px">
  4. <a-select-option value="1">Option 1</a-select-option>
  5. <a-select-option value="2">Option 2</a-select-option>
  6. <a-select-option value="3">Option 3</a-select-option>
  7. </a-select>
  8. </div>
  9. </template>
  10. <script>
  11. export default {
  12. data(){
  13. return {
  14. name: 'Option 1'
  15. }
  16. }
  17. }
  18. </script>
  19. <style lang="less" scoped>
  20. // 使用 scss, less 时,可以用 /deep/ 进行样式穿透
  21. .test-wrapper ::v-deep .ant-select {
  22. font-size: 26px;
  23. }
  24. .test-wrapper /deep/ .ant-select {
  25. font-weight: 700;
  26. }
  27. </style>
  28. <style scoped>
  29. /* 这里注释不可以用 `//` */
  30. .test-wrapper >>> .ant-select {
  31. color: blue
  32. }
  33. </style>

在 scss、less 中可以使用 /deep/::v-deep 进行样式穿透,在css 中可以使用 >>> 穿透。

最终渲染成:

  1. <style type="text/css">
  2. .test-wrapper[data-v-2b80bebf] .ant-select {
  3. font-size: 26px;
  4. }
  5. .test-wrapper[data-v-2b80bebf] .ant-select {
  6. font-weight: 700;
  7. }
  8. </style>
  9. <style type="text/css">
  10. /* 这里注释不可以用 `//` */
  11. .test-wrapper[data-v-2b80bebf] .ant-select {
  12. color: blue
  13. }
  14. </style>

请求

axios

首先回顾下 axios 如何使用的。

在 vue-admin-template(基于 element-ui) 中使用 axios 有以下几步:

  • 安装 axios 包
  • 对 axios 进行封装,比如封装到 request.js 文件中。关键增加请求拦截器和响应拦截器,比如返回 403、500等都会通过 Message 组件提示给用户
  • 每个页面(或模块)引入 request.js,导出接口。

ant-vue-pro 中axios 用法类似:

  • 通过 src\utils\request.js 封装 request.js
  • 每个页面(或模块)引入 request.js,导出接口。例如登录模块对应 src\api\login.js

为了方便管理维护,统一的请求处理都放在@/src/api 文件夹中,并且一般按照 model 纬度进行拆分文件,如:

  1. api/
  2. user.js
  3. permission.js
  4. goods.js
  5. ...
本地项目 api

本地项目的 api 大概是这样:

  1. import { axios } from '@/utils/request'
  2. import cancelAxios from 'axios'
  3. import qs from 'qs'
  4. /* 取消请求 */
  5. var CancelToken = cancelAxios.CancelToken
  6. export let cancellistApi
  7. // 列表
  8. export function list (parameter) {
  9. return axios({
  10. url: '/acms/demo/list',
  11. method: 'get',
  12. // params 参数用于将数据通过查询字符串的形式添加到请求的 URL 中。这种方式适用于 GET 请求
  13. params: parameter,
  14. cancelToken: new CancelToken(function (c) {
  15. cancellistApi = c
  16. }),
  17. // paramsSerializer 是 axios 的一个配置选项,用于将请求参数序列化为 URL 查询字符串格式
  18. // 比如转换开始结束时间的格式:rangeDate[]=2023-11-11&rangeDate[]=2023-12-03 转成 rangeDate=2023-11-11&rangeDate=2023-12-03
  19. paramsSerializer: function (params) {
  20. return qs.stringify(params, {
  21. arrayFormat: 'repeat'
  22. })
  23. }
  24. })
  25. }
  26. // get请求
  27. export function review (id) {
  28. return axios({
  29. url: `/acms/demo/detail/${id}`,
  30. method: 'get'
  31. })
  32. }
  33. // post请求
  34. export function pass (data) {
  35. return axios({
  36. url: `/acms/demo/pass`,
  37. method: 'post',
  38. // data 参数则是将数据作为请求的正文发送给服务器。这种方式适用于 POST、PUT、DELETE 等请求
  39. // 请求中的 Content-Type 头附带的是 application/json 或 multipart/form-data 等适合传递数据的类型
  40. data
  41. })
  42. }
  43. // 删除文章
  44. export function delArticle (id) {
  45. return axios({
  46. url: `/acms/article/${id}`,
  47. // DELETE 方法用于请求服务器删除指定的资源。它通常需要在请求中指定要删除的资源的标识符。例如,使用 DELETE 方法可以删除用户账号、删除文章等。
  48. method: 'delete'
  49. })
  50. }
  51. // 上线文章
  52. // PUT 方法用于向指定的 URL 发送数据,通常是用于更新服务器上的资源
  53. export function onlineArticle (id) {
  54. return axios({
  55. url: `/acms/article/online/${id}`,
  56. method: 'put'
  57. })
  58. }

get、post、put、delete请求,有时引入 qs 包,用于将请求的参数对象序列化,比如处理开始时间和结束时间。

Tipqs 是一个用于序列化和反序列化 URL 查询字符串的 JavaScript 库。比如:

  • 序列化:将 JavaScript 对象序列化为 URL 查询字符串的格式,以便作为请求参数添加到 URL 中。例如,将 { key1: 'value1', key2: 'value2' } 转换为 key1=value1&key2=value2。
  • 反序列化:将 URL 查询字符串解析为 JavaScript 对象,方便进行参数的提取和处理。例如,将 key1=value1&key2=value2 转换为 { key1: 'value1', key2: 'value2' }。
  • 处理复杂参数:qs 支持处理复杂对象、数组等数据结构,可以将它们转换为合适的 URL 查询字符串格式,方便进行网络请求。

cancelAxios 用于取消请求。不过有的同事用法不对,他用在搜索的 input 框中,想实现输入字符延迟查询。可以用 .lazy 或 lodash 的延迟。

Tip: ant design vue 中 lazy(<a-input v-model.lazy=)不起作用。根据场景可以使用lodash 中的防抖或节流。当调用 delayedRequest 函数时,如果在 1000 毫秒内没有再次调用该函数,那么延迟时间结束后,请求逻辑将会执行。

  1. import { debounce } from 'lodash';
  2. const delayedRequest = debounce(() => {
  3. // 在这里执行你的请求逻辑
  4. }, 1000); // 延迟时间为 1000 毫秒
  5. // 调用 delayedRequest 函数
  6. delayedRequest();
async和Promise

在 ant-vue-pro 中只使用了 Promise,没有使用 async

从 ./src/views 中搜索:

  • async、await 都没有
  • promise 在13个文件中有 23 处。

用法大致如下:

  1. // 模拟网络请求、卡顿 800ms
  2. new Promise((resolve) => {
  3. }).then(() => {
  4. })
  1. // 两个都成功才进入 then
  2. Promise.all([repositoryForm, taskForm]).then(values => {
  3. }).catch(() => {
  4. })
  1. new Promise((resolve) => {
  2. }).then(() => {
  3. }).catch(() => {
  4. // 总是会执行。比如关闭`加载中...`弹框
  5. }).finally(() => {
  6. })

本地项目的写法

本地项目的写法有以下几种:

  • 取消发布。只处理了成功的情况
  1. // axios 在响应拦截器中已经处理了http 非 200 的请求,也处理的 50004000 等 token 过期或其他错误,最后到这里通常是约定好的接口数据。
  2. cancelPublish(params).then((res) => {
  3. if (res.code === 0) {
  4. this.getDataList()
  5. this.$message.success(res.msg)
  6. }
  7. })
  • 编辑。在 finally 中关闭关闭加载中...弹框
  1. async editFn (contentType, params) {
  2. this.loading = true
  3. try {
  4. const res = await updateArticle(params)
  5. if (res.code === 0) {
  6. // 请求成功...
  7. }
  8. } catch (err) {
  9. } finally {
  10. this.loading = false
  11. }
  12. },
  • 只有一个异步请求,并且需要处理错误情况。可以这么写:
  1. fetchData(false).then(() => {
  2. // do ...
  3. }).catch(() => {
  4. console.log('error')
  5. })

如果不觉得 try...catch 麻烦,也可以这样:

  1. try {
  2. let p = await fetchData(false)
  3. // do ...
  4. } catch (e) {
  5. console.log('error')
  6. }

:try...catch 除了可以捕获语法报错,还能捕获 reject 。

  1. const fetchData = new Promise((resolve, reject) => {
  2. reject(11);
  3. });
  4. async function myAsyncFunction() {
  5. try {
  6. let p = await fetchData;
  7. console.log('p', p);
  8. // 其他代码...
  9. } catch (e) {
  10. console.log('error', e);
  11. }
  12. }
  13. myAsyncFunction();
  14. // => error 11
需要注意的几点
错误写法

遮罩层没有消失

  1. async function request() {
  2. console.log('开启遮罩')
  3. // 报错就退出了
  4. let json = await requestUserList() // {1}
  5. // 处理数据...
  6. console.log('关闭遮罩');
  7. }
await 与并行
  1. // 下面这段代码是串行
  2. async function foo() {
  3. let a = await createPromise(1)
  4. let b = await createPromise(2)
  5. }

可以通过下面两种方法改为并行:

  1. // 方式一
  2. async function foo() {
  3. let p1 = createPromise(1)
  4. let p2 = createPromise(2)
  5. // 至此,两个异步操作都已经发出
  6. await p1
  7. await p2
  8. }
  9. // 方式二
  10. async function foo() {
  11. let [p1, p2] = await Promise.all([createPromise(1), createPromise(2)])
  12. }
async 有时会比 Promise 更容易调试
promise.catch

以下两段代码等效

  1. promise1.then(null, () => {
  2. console.log('拒绝')
  3. })
  4. // 等价于
  5. promise1.catch(() => {
  6. console.log('拒绝')
  7. })
  • 链式捕获错误
  1. let p1 = new Promise((resolve, reject) => {
  2. resolve('10') // {1}
  3. })
  4. // 三个完成处理程序都有可能出错,我们可以在末尾添加一个已拒绝处理的程序对这个链式统一处理
  5. p1.then(() => {
  6. throw new Error('fail')
  7. console.log(1)
  8. }).then(() => {
  9. console.log(2)
  10. }).then(() => {
  11. console.log(3)
  12. }).catch(e => {
  13. console.log(e.message)
  14. })
  15. // 输出:fail

如果将 {1} 改成 reject(10),也会直接到 catch 中,这时 e 就是 10。

await 的返回值

await 命令后面是一个 Promise 对象。如果不是,会被转为一个立即 resolve 的 Promise 对象。Promise 的解决值会被当作该 await 表达式的返回值。

  1. async function fa() {
  2. return await 1
  3. }
  4. // 等价于
  5. async function fa() {
  6. return await Promise.resolve(1)
  7. }
  8. // 等价于
  9. async function foo() {
  10. return await new Promise((resolve, reject) => {
  11. resolve(1)
  12. })
  13. }

在看一个示例:

  1. function resolveAfter2Seconds(x) {
  2. return new Promise((resolve) => {
  3. setTimeout(() => {
  4. resolve(x);
  5. }, 2000);
  6. });
  7. }
  8. async function f1() {
  9. let x = await resolveAfter2Seconds(10).then(res => {return 1});
  10. console.log(x); // 1
  11. }
  12. f1();

每调用一次 then 就会创建一个新的 Promise。

async 函数中的 return

return 返回值,会成为 then() 方法回调函数的参数

  1. async function foo() {
  2. return 'hello'
  3. }
  4. foo().then(v => {
  5. console.log(v)
  6. })
  7. // hello
Promise 执行器错误

每个执行器中都隐含一个 try-catch 块,所以错误会被捕获并传入给已拒绝回调。以下两段代码等价:

  1. let p1 = new Promise(function(resolve, reject){
  2. throw new Error('fail')
  3. })
  4. p1.catch(v => {
  5. console.log(v.message) // fail
  6. })
  1. let p1 = new Promise(function(resolve, reject){
  2. try{
  3. throw new Error('fail')
  4. }catch(e){
  5. reject(e)
  6. }
  7. })
  8. ...

env

.env 是一种用来存储环境变量的文件。

模板项目的 env

在 ant-vue-pro 中一共有三个 .env 文件。

  1. // .env
  2. NODE_ENV=production
  3. VUE_APP_PREVIEW=false
  4. VUE_APP_API_BASE_URL=/api
  5. // .env.development
  6. NODE_ENV=development
  7. VUE_APP_PREVIEW=true
  8. VUE_APP_API_BASE_URL=/api
  9. // .env.preview
  10. NODE_ENV=production
  11. VUE_APP_PREVIEW=true
  12. VUE_APP_API_BASE_URL=/api

.env - 在所有的环境中被载入
.env.[mode] - 只在指定的模式中被载入

一个环境文件只包含环境变量的“键=值”对:

  1. FOO=bar
  2. VUE_APP_NOT_SECRET_CODE=some_value
  3. FOO2 = bar # 等号前后加空格也可以

:只有 NODE_ENVBASE_URL 和以 VUE_APP_ 开头的变量默认可以被识别。比如 FOO=bar 就不会被识别,除非使用其他手段进行变量扩展。

三个模式

默认情况下,一个 Vue CLI 项目有三个模式:

  • development 模式用于 vue-cli-service serve
  • test 模式用于 vue-cli-service test:unit
  • production 模式用于 vue-cli-service build 和 vue-cli-service test:e2e

比如配置如下:

  1. // .env
  2. NODE_ENV=production
  3. VUE_APP_PREVIEW=false
  4. VUE_APP_API_BASE_URL=/api
  5. VUE_APP_address = 长沙
  6. // .env.development
  7. VUE_APP_PREVIEW=true
  8. VUE_APP_API_BASE_URL=/api
  9. tel = 2222
  10. VUE_APP_NAME=peng 3
  11. VUE_APP_tel = 1111

运行 npm run serve(对应package.json中 "serve": "vue-cli-service serve",),会依次加载 .env 和 .env.development,后者会将前者的值覆盖,所以最后通过 process.env 输出:

  1. console.log(process.env)
  2. {
  3. BASE_URL: "/"
  4. NODE_ENV: "development"
  5. VUE_APP_API_BASE_URL: "/api"
  6. VUE_APP_NAME: "peng 3"
  7. VUE_APP_PREVIEW: "true"
  8. VUE_APP_address: "长沙"
  9. VUE_APP_tel: "1111"
  10. }

比如:

  • VUE_APP_address 从 .env 中得到
  • tel 被忽略
  • NODE_ENV 在 .env.development 中被自动加上该属性
  • VUE_APP_tel 中 = 前后有空格也能生效

Tip:每次修改 .env,需要重新启动服务才会生效。

--mode

可以通过 --mode 覆写默认的模式。比如本地开发我可以代理到测试的url,也像代理到预发布的url,我可以这样做:
增加 .env.pre:

VUE_APP_URL=/myapi

package.json 增加:

  1. "scripts": {
  2. "serve": "vue-cli-service serve",
  3. + "serve:pre": "vue-cli-service serve --mode pre",

通过 npm run serve:pre 就能操作预发布环境的数据。现在输出:

  1. console.log(process.env)
  2. {
  3. BASE_URL: "/"
  4. NODE_ENV: "development"
  5. VUE_APP_API_BASE_URL: "/api"
  6. VUE_APP_PREVIEW: "false"
  7. VUE_APP_URL: "/myapi"
  8. VUE_APP_address: "长沙"
  9. }

注意,现在 NODE_ENV 是 development,这个值来自 .env,vue-cli 没有给我们增加一个 NODE_ENV 的变量。

execSync

关于构建,有的人可能会通过配置一个 js 去执行,这样能更灵活,比如运维需要你创建一个每次创建一个文件。就像这样:

  1. // package.json
  2. "scripts": {
  3. "build": "node src/libs/shell.js",
  4. "build:pre": "node src/libs/shell.js pre",
  5. "build:test": "node src/libs/shell.js test"
  1. // shell.js
  2. var dist = 'dist'
  3. var d = new Date().getTime().toString()
  4. var env = process.argv.splice(2)[0]
  5. var writeFileSync = require('fs').writeFileSync
  6. var execSync = require('child_process').execSync
  7. if (env == 'test') {
  8. execSync('vue-cli-service build --mode test')
  9. } else if (env == 'pre') {
  10. execSync('vue-cli-service build --mode pre')
  11. } else {
  12. execSync('vue-cli-service build --mode prod')
  13. }
  14. writeFileSync(dist + '/xx.txt', d)

Tip: execSync 是 Node.js 的 child_process 模块提供的一个同步执行外部命令的函数。它允许通过 JavaScript 代码来执行系统命令,并等待命令执行完成后再继续执行后续代码。

Mock

ant-vue-pro 使用的是 mockjs2(好像和 mockjs 是同一个东西)。

本地项目使用的 proxy,当后端没有提供接口给前端时,前端还是需要自己去模拟数据。

笔者通过如下方式给模板项目添加 mockjs。

首先模板项目中有 ant-vue-pro 中的 mockjs2 包,直接跳过安装包。

创建 src/mock/index.js:

  1. // 判断环境不是 prod 时加载 mock 服务
  2. if (process.env.NODE_ENV !== 'production') {
  3. console.log('[antd-pro] mock mounting')
  4. const Mock = require('mockjs2')
  5. require('./skin.js')
  6. Mock.setup({
  7. timeout: 100 // 设置所有请求的响应时间为100ms
  8. })
  9. }
  1. // skin.js
  2. import Mock from 'mockjs2'
  3. const navList = {
  4. "code": 0,
  5. "msg": "查询成功",
  6. "error": "",
  7. "url": null,
  8. "data": [
  9. {...},
  10. ],
  11. "success": true
  12. };
  13. Mock.mock(/\/mockjs-cms\/channel\/list\/navigation/, 'get', navList)

main.js 中引入 src/mock/index:

import './mock/index.js'

最后是发起请求:

  1. export function queryNavigation() {
  2. return axios({
  3. url: `/mockjs-cms/channel/list/navigation`,
  4. method: 'get',
  5. })
  6. }

:中途笔者遇到两个问题:

  • mockjs 返回了导航数据,但页面没有显示导航。将 timeout: 800 改成 timeout: 100
  • 本地开发时,保存 mock/index.js(按 ctrl + s) 文件 vscode 不会触发自动编译,在别的文件中保存会触发自动编译。最后引入这个资源,比如在 main.js 中引入,然后重启服务或vscode即可实现保存自动编译。

另外公司使用了 eolink,后端定义好接口后,就有一个简易 Mock 链接,开发阶段前端可以这样:

  1. export function queryNavigation() {
  2. return axios({
  3. // 简易 Mock 链接
  4. url: 'https://mockapi.eolink.com/81F5kJv4c4f5ff6d3b8a7880xxxx',
  5. method: 'get',
  6. })
  7. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/530566
推荐阅读
相关标签
  

闽ICP备14008679号