快速摸一个 Ant design vue 项目
初始化项目
-
用 vue-cli 快速构建项目
vue create ant-design-vue-pro
-
cd ant-design-vue-pro/
-
安装必要依赖
npm i ant-design-vue moment
-
删除/初始化 不需要的文件
// clear └──src/ ├───router/ │ └───index.js ├───views/ │ └───Home.vue └───App.vue
-
引入 ant-design-vue
import Antd from "ant-design-vue"; import "ant-design-vue/dist/antd.css";
debugger
import "ant-design-vue/dist/antd.less"; // 报错 Syntax Error: // https://github.com/ant-design/ant-motion/issues/44 .bezierEasingMixin(); // 解决方案:开启 javascript css: { loaderOptions: { less: { loader: "less-loader", options: { javascriptEnabled: true, }, }, }, },
按需引入 UI 组件
import Button from "ant-design-vue/lib/button";
import "ant-design-vue/lib/button/style";
- babel-plugin-import
- 修改 babel.config.js 文件,配置 babel-plugin-import
module.exports = { presets: ["@vue/app"], + plugins: [ + [ + "import", + { libraryName: "ant-design-vue", libraryDirectory: "es", style: true } + ] + ] };
- src/main.js
- import Button from 'ant-design-vue/lib/button'; + import { Button } from 'ant-design-vue'; - import 'ant-design-vue/dist/antd.css'
bug
// ❌ 无法全局引入 import Antd from 'antd-design-vue
- 修改 babel.config.js 文件,配置 babel-plugin-import
高扩展性的路由
-
现有方案
- 基于配置
- 基于约定:轮子根据文件结构生成路由
-
component
const routes = [ { path: "/user", component: () => import(/* webpackChunkName: user */ "./component/RenderRouterView.vue"), children: [ //... ], }, ];
const routes = [ { path: "/user", component: { render: (h) => h("router-view") }, children: [ //... ], }, ];
-
NProgress.start()
— shows the progress barNProgress.set(0.4)
— sets a percentageNProgress.inc()
— increments by a littleNProgress.done()
— completes the progress
可动态改变的页面布局
- 通过路由传递配置变量
如何将菜单和路由结合
-
约定
- 在 routes 中添加 标志位,筛选需要渲染到菜单的路由项。
hideInMenu: true
- 处理 routes 中的嵌套路由逻辑,约定有
name
字段才进行渲染 - 隐藏子路由
hideChildrenMenu
,处理 “页面在子路由时,菜单依然高亮” 的逻辑 - 添加显示的元信息
meta
,icon / title ...
- 在 routes 中添加 标志位,筛选需要渲染到菜单的路由项。
-
根据约定,生成动态菜单
const menuData = getMenuData(this.$router.options.routes); getMenuData(routes){ }
-
利用函数式组件(无状态,只接受参数) + 组件递归,渲染处理后的 routes 对象。
-
.sync
修饰符- 在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件都没有明显的变更来源。
- 这也是为什么我们推荐以 update:myPropName 的模式触发事件取而代之。
- 举个例子,在一个包含 title prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:
this.$emit('update:title', newTitle)
- 父组件
<text-document v-bind:title="doc.title" v-on:update:title="doc.title = $event" ></text-document>
- 为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符:
<text-document v-bind:title.sync="doc.title"></text-document>
如何使用路由进行权限管理
-
权限验证相关函数
export async function getCurrentAuthority() { const { role } = await this.$axios.$get("/user"); return ["admin"]; } // some() 方法测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个Boolean类型的值。 export function check(authority) { const current = getCurrentAuthority(); return current.some((item) => authority.includes(item)); } export function isLogin() { const current = getCurrentAuthority(); return current && current[0] !== "guest"; }
-
路由守卫
import findLast from "lodash/findLast"; import { check, isLogin } from "utils/auth"; router.beforeEach((to, from, next) => { // ... const record = findLast(to.matched, (item) => item.meta.authority); if (record && !check(record.meta.authority)) { if (!isLogin() && to.path !== "/user/login") { next({ path: "/user/login" }); } else if (to.path !== "/403") { next({ path: "/403" }); } // loading = false // ... } // ... });
-
侧边栏鉴权
routes.forEach((item) => { if (item.meta && item.meta.authority && !check(item.meta.authority)) { return; } });
-
403 添加弹窗提醒
import { notifiction } from "ant-deisgn-vue"; if (to.path !== "/403") { notifiction.error({ message: "403", description: "您没有权限访问该页面,请联系管理员", }); next({ path: "/403" }); }
更加精细的权限设计(权限组件、权限指令)
-
权限组件 - 函数式组件
export default { functional: true, render: function (h, context) { const { props, scopeSlots } = context; return check(props.authority) ? scopeSlots.default() : null; }, };
-
权限指令 - 插件式
export function install(Vue, options = {}) { const { name = "auth" } = options; Vue.directive(name, { // 当被绑定的元素插入到 DOM 中时…… inserted: function (el, binding) { if (!check(binding.value)) { el.parentNode && el.parentNode.removeChild(el); } }, }); }
-
比较
- 指令在 inserted 时 remove 后,当权限动态改变时,无法重新添加 el。
- 组件的响应更灵活,但使用需要嵌套目标 el。
如何在组件中使用ECharts、Antv等其他第三方库
-
- 插入所需的图表组件。
- 抽象可配置参数。
- 优化(防抖)
- 添加更多需求(动态改变数据)
-
echart 渲染宽度超出容器?
- 因为 echart 是在真正渲染完成前获取高度。
- 解决:
import { addListener, removeListener } from 'resize-detector'
-
resize 时,添加防抖
- lodash/debounce
- 该函数会从上一次被调用后,延迟
wait
毫秒后调用func
方法。 - 提供一个
cancel
方法取消延迟的函数调用以及 flush 方法立即调用 options.leading
与|或options.trailing
决定延迟前后如何触发(注:是 先调用后等待 还是 先等待后调用)。
- 该函数会从上一次被调用后,延迟
-
created(){ this.resize = debounce(this.resize, 300) }
- lodash/debounce
-
监听 option 变化
- 深度监听: 耗性能( Vue3 劫持整个对象 )
export default { watch: { option: { handler: () => {}, deep: true, }, }, };
- 手动替换整个对象
option = {...option}
- 深度监听: 耗性能( Vue3 劫持整个对象 )
如何高效使用 Mock 数据进行开发
- 剥离 mock 数据和业务代码
- axios npm
- 配置 webpack/devserver
- mock 数据不更新:清除指定模块缓存
require.cache
: 被引入的模块将被缓存在这个对象中。require.resolve
:在 node 中,可以使用 require.resolve 来查询某个模块的完整路径delete require.cache[require.resolve(name)]
-
module.exports = { devServer: { proxy: { "/api": { target: "http://localhost:8081", bypass: function (req, res) { if (req.headers.accept.indexOf("html") !== -1) { console.log("Skipping proxy for browser request."); return "/index.html"; } else { // 根据约定寻找文件 const name = req.path.split("/api/")[1].split("/").join("_"); const mock = require(`./mock/${name}`); const result = mock(req.method); // 清理模块缓存 require.cache(require.resolve(`./mock/${name}`)); return res.send(result); } }, }, }, }, };
如何与服务端进行交互(Axios)
-
添加环境变量
MOCK
-
- 是什么?
运行跨平台设置和使用环境变量(Node 中的环境变量)的脚本。
- 为什么需要?
我们在自定义配置环境变量的时候,由于在不同的环境下,配置方式也是不同的。例如在 window 和 linux 下配置环境变量。
- 是什么?
-
package.json
-
{ "scripts": { "serve:no-mock": "cross-env MOCK=NONE " } }
const app = new (require("koa"))(); const mount = require("koa-mount"); app.use( mount("/api/dashboard/chart", async (ctx) => { ctx.body = [10, 20, 30, 40, 50]; }) ); app.listen(8081);
-
-
axios 拦截:二次封装,统一错误处理
- request.js
import axios from "axios"; function request(options) { return axios(options) .then((res) => { return res; }) .catch((error) => { const { response: { status, statusText }, } = error; notifiction.error({ message: status, describtion: statusText, }); return Promise.reject(error); }); }
Vue.prototype.$request = request
- jsx:
@vue/babel-preset-jsx
- request.js
创建一个分步表单
- vuex: 临时存储表单数据
- modules/form.js
const state = () => ({ step: { payAccount: "" } }); const mutation = { saveStepFormData(state, payload) { state.step = { ...state.step, ...payload }; }, }; const actions = { async submitStepForm({ commit }, payload) { await request({ method: "POST", url: "", data: payload }); // 不应该是清空表单吗? commit("saveStepFormData", payload); router.push(""); }, }; export default { namespaced: true, state, mutations, actions, };
- modules/form.js
如何管理系统中的图标
- 来自 iconfont
import { Icon } from "ant-design-vue"; const IconFont = Icon.createFromIconfontCN({ scriptUrl: "" });
<icon-font type="icon-404" />
- svg
<image url>
- 手动注册 component / 利用 svg-loader 转换成 component
- 查看 vue cli 内部配置
vue inspect > output.js
如何定制主题及动态切换主题
-
全局:config 配置
module.exports = { css: { loaderOption: { less: { modifyVars: { "primary-color": "#1DA57A", "link-color": "#1DA57A", "border-radius-base": "2px", }, }, }, }, };
-
局部:深度作用选择器
如果你希望 scoped 样式中的一个选择器能够作用得“更深”,例如影响子组件,你可以使用 >>> 操作符:
<style scoped> .a >>> .b { /* ... */ } </style>
-
在线动态编译主题色
- 耗性能,
- 如有需求,可以在本地编译好多个主题样式文件,再从从服务端拉取
- antd-theme-webpack-plugin
- 该 webpack 插件用于生成特定于颜色的 less / css 并将其注入到 index.html 文件中,以便您可以在浏览器中更改 Ant Design 特定的颜色主题。
国际化
-
antd-vue 组件库国际化:localProvider -> configProvider
<template> <div id="app"> <a-config-provider :locale="locale"> </a-config-provider> </div> </template>
import zhCN from "ant-design-vue/lib/locale-provider/zh_CN"; import enUS from "ant-design-vue/lib/locale-provider/en_US"; export default = { data(){ return { locale: enUS } }, watch:{ "$route.query.locale"(val){ this.locale = val === 'enUS'? enUS : zhCN } } }
-
moment 国际化
import moment from 'moment'; export default={ watch:{ "$route.query.locale"(val){ moment.locale(val==='enUS'?'en':'zh_cn'); } }}
-
业务代码国际化:VueI18n
-
main.js
import VueI18n from "vue-i18n"; import zhCN from "./locale/zhCN"; import enUS from "./locale/enUS"; import queryString from "query-string"; const i18n = new VueI18n({ locale: queryString.parse(location.search).locale || "zhCN", message: { zhCN: { message: zhCN, }, enUS: { message: enUS, }, }, }); new Vue({ router, store, i18n, render: (h) => h(App), }).$mount("#app");
-
zhCN.js / enUS.js
export default { "app.workspace.title": "时间", };
export default { "app.workspace.title": "TIME", };
-
workspace.vue
<template> {{$t('message')['app.workspace.title']}} </template>
-
handleLocale
export default { watch: { "$route.query.locale"(val) { this.$i18n.locale = val; }, }, };
-
如何高效地构建打包方式
打包分析报告:( VUE CLI )
npm run build -- --report
-
UI 组件按需加载 / babel
-
router 中使用
webpackChunkName
,对路由进行懒加载和拆包。 -
按需引入 lodash
-
import debounce from 'lodash/debounce'
-
使用插件 lodash-webpack-plugin
npm i lodash-webpack-plugin babel-plugin-lodash -D
babel.config.js
module.exports = { presets: ["@vue/cli-plugin-babel/preset", "@vue/babel-preset-jsx"], plugins: ["lodash"], };
vue.config.js
const LodashModuleReplacementPlugin = require("lodash-webpack-plugin"); module.exports = { chainWebpack: (config) => { config .plugin("loadshReplace") .use(new LodashModuleReplacementPlugin()); }, };
-
lodash-es 结合 tree-shaking
import { debounce } from 'lodash-es'
tree-shaking 的作用,即移除上下文中未引用的代码(dead code)
只有当函数给定输入后,产生相应的输出,且不修改任何外部的东西,才可以安全做 shaking 的操作
如何使用tree-shaking?
-
确保代码是 es6 格式,即 export,import
-
package.json 中,设置 sideEffects
-
确保 tree-shaking 的函数没有副作用
-
babelrc 中设置
presets [["env", { "modules": false }]]
禁止转换模块,交由 webpack 进行模块化处理 -
结合 uglifyjs-webpack-plugin
-
-
如何构建可交互的组件文档
- raw-loader + highlightjs
main.js
view.vueimport hljs from "highlight.js"; import "highlight.js/styles/github.css"; Vue.use(hljs.vuePlugin);
<highlightjs language="javascript" :code="ChartCode" />
- 自己编写 loader:如 md-loader(成本高)
如何做好组件的单元测试
-
auth.spec.js
import { authCurrent, check } from "@/utils/auth.js"; describe("auth test", () => { it("empty auth", () => { authCurrent.splice(0, authCurrent.length); expect(check(["user"])).toBe(false); expect(check(["admin"])).toBe(false); }); });
-
jest.config.js
module.exports = { preset: "@vue/cli-plugin-unit-jest", moduleNameMapper: { "^@/(.*)$": "<rootDir>/src/$1", }, resolver: null, collectCoverage: process.env.COVERAGE === "true", collectCoverageFrom: ["src/**/*.{js,vue}", "!**/node_modules/**"], };
如何发布组件到 NPM
- 注册 npm 账号,填写 用户名、密码和邮箱;
- 进入项目文件夹
- 使用
npm login
,登录自己的 npm 账号; - 使用
npm publish
,·发布自己的包到 npm; - 查看自己发布的包是否成功,可以去别的项目执行
npm install
你发布的包名,下载成功。
注意
- 发布自己包之前,应先去 npm 官网搜索自己要发布的包名是否已经存在,已存在的包名会提交失败;
- 自己发布的包更新时,每次都要到 package.json, 将 version 修改,例如:从 1.0.0 改为 1.0.1。然后再执行 npm publish 更新;
GitHub相关生态应用(CI 持续集成、单车覆盖率、文档发布、issue管理)
- CI 持续集成
- 单测覆盖率(报告作为用户选择项目的重要参考)
- 文档托管
- github.io
- gitee.io
- www.netlify.com/
- 管理issue(bug&功能请求)
- github.com/offu/close-… (自动关闭issue)
- vuecomponent.github.io/issue-helpe… (issue 模版)
- github.com/dessant/loc… (锁定已关闭的issue)
转载自:https://juejin.cn/post/6954188375263969287