网站首页 > 开源技术 正文
上一篇,认识Docker
1. 创建一个vue项目
相信大部分人都已经知道怎么创建项目的,可以跳过这一节,看下一节。
1.1 安装@vue/cli
# 全局安装 vue-cli脚手架 npm install -g @vue/cli
等待安装完成后开始下一步
1.2 初始化项目
vue create vue-cli3-project
- 选择一个预设
可以选择默认预设,默认预设包含了babel,eslint我们选择更多功能 Manually select features
回车后来到选择插件
插件选择
这边选择了(Babel、Router、Vuex、Css预处理器、Linter / Formatter 格式检查、Unit测试框架)
路由模式选择
是否使用 history模式的路由 (Yes)
选择一个css预处理器 (Sass/SCSS)
选择一个eslint配置
这边选择 ESLint + Standard config,个人比较喜欢这个代码规范
选择什么时候进行 eslint 校验
选择(Lint on save)保存是检查
如果你正在使用的vscode编辑器的话,可以配置eslint插件进行代码自动格式化
选择测试框架 (Mocha + Chai)
选择将这些配置文件写入到什么地方 (In dedicated config files)
是否保存这份预设配置?(y)
选是的话,下次创建一个vue项目,可以直接使用这个预设文件,而无需再进行配置。
2. 全局组件自动注册
在components目录下创建一个global目录,里面放置一些需要全局注册的组件。
index.js作用只要是引入main.vue,导出组件对象
在components中创建一个index.js,用来扫描全局对象并自动注册。
// components/index.js import Vue from 'vue' // 自动加载 global 目录下的 .js 结尾的文件 const componentsContext = require.context('./global', true, /\.js$/) componentsContext.keys().forEach(component => { const componentConfig = componentsContext(component) /** * 兼容 import export 和 require module.export 两种规范 */ const ctrl = componentConfig.default || componentConfig Vue.component(ctrl.name, ctrl) })
最后在入口文件main.js中导入这个index.js中就可以了
3.路由自动引入
在 Vue项目中使用路由,相信想熟的人已经很熟悉怎么使用了,要新增一个页面的话,需要到路由配置中配置该页面的信息。
如果页面越来越多的话,那么如何让我们的路由更简洁呢?
3.1 拆分路由
根据不同的业务模块进行拆分路由
在每个子模块中导出一个路由配置数组
在根 index.js中导入所有子模块
3.2 自动扫描子模块路由并导入
当我们的业务越来越庞大,每次新增业务模块的时候,我们都要在路由下面新增一个子路由模块,然后在index.js中导入。
那么如何简化这种操作呢?
通过上面的自动扫描全局组件注册,我们也可以实现自动扫描子模块路由并导入
4. 通过node来生成组件
作为前端开发者,放着 node这么好用的东西如果不能运用起来,岂不是很浪费?
虽然我们通过上面已经实现了组件的自动注册,不过每次新建组件的时候,都要创建一个目录,然后新增一个.vue文件,然后写template、script、style这些东西,然后新建一个index.js、导出vue组件、虽然有插件能实现自动补全,但还是很麻烦有木有。那么我们能不能通过node来帮助我们干这些事情呢?只要告诉node帮我生成的组件名称就行了。其它的事情让node来干
4.1 通过node来生成组件
- 安装一下chalk,这个插件能让我们的控制台输出语句有各种颜色区分
npm install chalk --save-dev
在根目录中创建一个 scripts 文件夹,
新增一个generateComponent.js文件,放置生成组件的代码、
新增一个template.js文件,放置组件模板的代码
- template.js
// template.js module.exports = { vueTemplate: compoenntName => { return `<template> <div class="${compoenntName}"> ${compoenntName}组件 </div> </template> <script> export default { name: '${compoenntName}' } </script> <style lang="scss" scoped> .${compoenntName} { } </style> ` }, entryTemplate: `import Main from './main.vue' export default Main` }
generateComponent.js(可以直接复制代码查看运行效果)
// generateComponent.js` const chalk = require('chalk') const path = require('path') const fs = require('fs') const resolve = (...file) => path.resolve(__dirname, ...file) const log = message => console.log(chalk.green(`${message}`)) const successLog = message => console.log(chalk.blue(`${message}`)) const errorLog = error => console.log(chalk.red(`${error}`)) const { vueTemplate, entryTemplate } = require('./template') const generateFile = (path, data) => { if (fs.existsSync(path)) { errorLog(`${path}文件已存在`) return } return new Promise((resolve, reject) => { fs.writeFile(path, data, 'utf8', err => { if (err) { errorLog(err.message) reject(err) } else { resolve(true) } }) }) } log('请输入要生成的组件名称、如需生成全局组件,请加 global/ 前缀') let componentName = '' process.stdin.on('data', async chunk => { const inputName = String(chunk).trim().toString() /** * 组件目录路径 */ const componentDirectory = resolve('../src/components', inputName) /** * vue组件路径 */ const componentVueName = resolve(componentDirectory, 'main.vue') /** * 入口文件路径 */ const entryComponentName = resolve(componentDirectory, 'index.js') const hasComponentDirectory = fs.existsSync(componentDirectory) if (hasComponentDirectory) { errorLog(`${inputName}组件目录已存在,请重新输入`) return } else { log(`正在生成 component 目录 ${componentDirectory}`) await dotExistDirectoryCreate(componentDirectory) // fs.mkdirSync(componentDirectory); } try { if (inputName.includes('/')) { const inputArr = inputName.split('/') componentName = inputArr[inputArr.length - 1] } else { componentName = inputName } log(`正在生成 vue 文件 ${componentVueName}`) await generateFile(componentVueName, vueTemplate(componentName)) log(`正在生成 entry 文件 ${entryComponentName}`) await generateFile(entryComponentName, entryTemplate) successLog('生成成功') } catch (e) { errorLog(e.message) } process.stdin.emit('end') }) process.stdin.on('end', () => { log('exit') process.exit() }) function dotExistDirectoryCreate(directory) { return new Promise((resolve) => { mkdirs(directory, function () { resolve(true) }) }) } // 递归创建目录 function mkdirs(directory, callback) { var exists = fs.existsSync(directory) if (exists) { callback() } else { mkdirs(path.dirname(directory), function () { fs.mkdirSync(directory) callback() }) } }
- 配置package.json
"new:comp": "node ./scripts/generateComponent" 复制代码
- 执行
如果使用 npm 的话 就是
npm run new:comp
如果使用 yarn 的话 就是
yarn new:comp
4.2 通过node来生成页面组件
通过上面的逻辑代码我们可以通过node来生成组件了,那么也可以举一反三来生成页面组件。只需稍微修改一下生成组件代码的逻辑。 在scripts目录下新建一个generateView.js文件
// generateView.js const chalk = require('chalk') const path = require('path') const fs = require('fs') const resolve = (...file) => path.resolve(__dirname, ...file) const log = message => console.log(chalk.green(`${message}`)) const successLog = message => console.log(chalk.blue(`${message}`)) const errorLog = error => console.log(chalk.red(`${error}`)) const { vueTemplate } = require('./template') const generateFile = (path, data) => { if (fs.existsSync(path)) { errorLog(`${path}文件已存在`) return } return new Promise((resolve, reject) => { fs.writeFile(path, data, 'utf8', err => { if (err) { errorLog(err.message) reject(err) } else { resolve(true) } }) }) } log('请输入要生成的页面组件名称、会生成在 views/目录下') let componentName = '' process.stdin.on('data', async chunk => { const inputName = String(chunk).trim().toString() /** * Vue页面组件路径 */ let componentVueName = resolve('../src/views', inputName) // 如果不是以 .vue 结尾的话,自动加上 if (!componentVueName.endsWith('.vue')) { componentVueName += '.vue' } /** * vue组件目录路径 */ const componentDirectory = path.dirname(componentVueName) const hasComponentExists = fs.existsSync(componentVueName) if (hasComponentExists) { errorLog(`${inputName}页面组件已存在,请重新输入`) return } else { log(`正在生成 component 目录 ${componentDirectory}`) await dotExistDirectoryCreate(componentDirectory) } try { if (inputName.includes('/')) { const inputArr = inputName.split('/') componentName = inputArr[inputArr.length - 1] } else { componentName = inputName } log(`正在生成 vue 文件 ${componentVueName}`) await generateFile(componentVueName, vueTemplate(componentName)) successLog('生成成功') } catch (e) { errorLog(e.message) } process.stdin.emit('end') }) process.stdin.on('end', () => { log('exit') process.exit() }) function dotExistDirectoryCreate(directory) { return new Promise((resolve) => { mkdirs(directory, function () { resolve(true) }) }) } // 递归创建目录 function mkdirs(directory, callback) { var exists = fs.existsSync(directory) if (exists) { callback() } else { mkdirs(path.dirname(directory), function () { fs.mkdirSync(directory) callback() }) } }
- 配置package.json 新增一个scripts脚本
"new:view": "node ./scripts/generateView" 复制代码
- 执行
如果使用 npm 的话 就是
npm run new:view
如果使用 yarn 的话 就是
yarn new:view
5. axios封装
- 安装 axios
npm install axios --save // or yarn add axios
5.1 配置不同的环境
在根目录新建三个环境变量文件
分别输入不同的地址, 比如dev就写 dev的api地址、test就写test的api地址
# // .env NODE_ENV = "development" BASE_URL = "https://easy-mock.com/mock/5c4c50b9888ef15de01bec2c/api"
接着在根目录中新建一个 vue.config.js
// vue.config.js module.exports = { chainWebpack: config => { // 这里是对环境的配置,不同环境对应不同的BASE_URL,以便axios的请求地址不同 config.plugin('define').tap(args => { args[0]['process.env'].BASE_URL = JSON.stringify(process.env.BASE_URL) return args }) } }
然后在src目录下新建一个 api文件夹,创建一个 index.js用来配置 axios的配置信息
// src/api/index.js import axios from 'axios' import router from '../router' import { Message } from 'element-ui' const service = axios.create({ // 设置超时时间 timeout: 60000, baseURL: process.env.BASE_URL }) // post请求的时候,我们需要加上一个请求头,所以可以在这里进行一个默认的设置 // 即设置post的请求头为application/x-www-form-urlencoded;charset=UTF-8 service.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'' export default service
5.2 请求响应封装
import axios from 'axios' import router from '../router' import { Message } from 'element-ui' const service = axios.create({ // 设置超时时间 timeout: 60000, baseURL: process.env.BASE_URL }) /** * 请求前拦截 * 用于处理需要在请求前的操作 */ service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }, (error) => { return Promise.reject(error) }) /** * 请求响应拦截 * 用于处理需要在请求返回后的操作 */ service.interceptors.response.use(response => { const responseCode = response.status // 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据 // 否则的话抛出错误 if (responseCode === 200) { return Promise.resolve(response) } else { return Promise.reject(response) } }, error => { // 服务器返回不是 2 开头的情况,会进入这个回调 // 可以根据后端返回的状态码进行不同的操作 const responseCode = error.response.status switch (responseCode) { // 401:未登录 case 401: // 跳转登录页 router.replace({ path: '/login', query: { redirect: router.currentRoute.fullPath } }) break // 403: token过期 case 403: // 弹出错误信息 Message({ type: 'error', message: '登录信息过期,请重新登录' }) // 清除token localStorage.removeItem('token') // 跳转登录页面,并将要浏览的页面fullPath传过去,登录成功后跳转需要访问的页面 setTimeout(() => { router.replace({ path: '/login', query: { redirect: router.currentRoute.fullPath } }) }, 1000) break // 404请求不存在 case 404: Message({ message: '网络请求不存在', type: 'error' }) break // 其他错误,直接抛出错误提示 default: Message({ message: error.response.data.message, type: 'error' }) } return Promise.reject(error) }) export default service
Message 方法是 element-ui 提供的一个消息提示组件、大家可以根据自己的消息提示组件进行替换
5.3 断网处理
在响应拦截中添加处理逻辑
service.interceptors.response.use(response => { const responseCode = response.status // 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据 // 否则的话抛出错误 if (responseCode === 200) { return Promise.resolve(response.data) } else { return Promise.reject(response) } }, error => { // 断网 或者 请求超时 状态 if (!error.response) { // 请求超时状态 if (error.message.includes('timeout')) { console.log('超时了') Message.error('请求超时,请检查网络是否连接正常') } else { // 可以展示断网组件 console.log('断网了') Message.error('请求失败,请检查网络是否已连接') } return } // 省略其它代码 ······ return Promise.reject(error) })
5.4 封装图片上传
// src/api/index.js export const uploadFile = formData => { const res = service.request({ method: 'post', url: '/upload', data: formData, headers: { 'Content-Type': 'multipart/form-data' } }) return res }
调用
async uploadFile (e) { const file = document.getElementById('file').files[0] const formdata = new FormData() formdata.append('file', file) await uploadFile(formdata) }
5.5 请求 显示 Loading 效果
let loading = null service.interceptors.request.use(config => { // 在请求先展示加载框 loading = Loading.service({ text: '正在加载中......' }) // 省略其它代码 ······ return config }, (error) => { return Promise.reject(error) }) service.interceptors.response.use(response => { // 请求响应后关闭加载框 if (loading) { loading.close() } // 省略其它代码 ······ }, error => { // 请求响应后关闭加载框 if (loading) { loading.close() } // 省略其它代码 ······ return Promise.reject(error) })
6. 巧用 Mixins
6.1 封装 store 公用方法
假设有这样一个场景,我们通过 vuex 封装了获取新闻列表的 function
import Vue from 'vue' import Vuex from 'vuex' import { getNewsList } from '../api/news' Vue.use(Vuex) const types = { NEWS_LIST: 'NEWS_LIST' } export default new Vuex.Store({ state: { [types.NEWS_LIST]: [] }, mutations: { [types.NEWS_LIST]: (state, res) => { state[types.NEWS_LIST] = res } }, actions: { [types.NEWS_LIST]: async ({ commit }, params) => { const res = await getNewsList(params) return commit(types.NEWS_LIST, res) } }, getters: { getNewsResponse (state) { return state[types.NEWS_LIST] } } })
然后在新闻列表页,我们通过 mapAction、mapGetters来调用Action和getters 我们需要写上这些代码
import { mapActions, mapGetters } from 'vuex' computed: { ...mapGetters(['getNewsResponse']) }, methods: { ...mapActions(['NEWS_LIST']) }
在假设,在另一个页面又需要重新调用获取新闻列表的接口,我们又要在写一遍上面的代码对吧?
复制粘贴就是干有木有?
如果接口突然加了一个参数,那岂不是每个要用到这个接口的代码都得加这个参数。
复制粘贴一时爽,需求一改你就爽
既然是重复的代码,我们肯定要复用,这时候Vue提供的Mixin就起了大作用了
- 封装 news-mixin.js 在 src下创建一个mixins目录,用来管理所有的mixins 新建一个news-mixin.js
import { mapActions, mapGetters } from 'vuex' export default { computed: { ...mapGetters(['getNewsResponse']) }, methods: { ...mapActions(['NEWS_LIST']) } }
然后在需要用到的组件中引入这个mixin,就能直接调用这个方法了。不管多少个页面,只要引入这个mixin,直接就能使用。
需求一改的话,也只需要修改这个mixin文件
// news/index.vue import Vue from 'vue' import newsMixin from '@/mixins/news-mixin' export default { name: 'news', mixins: [newsMixin], data () { return {} }, async created () { await this.NEWS_LIST() console.log(this.getNewsResponse) } }
6.2 扩展
除了封装 vuex 的公用方法,其实还有很多的东西也能做封装。例如:分页对象,表格数据,公用方法、等等就不一一举例了。可以看github
在多个地方经常使用,就可以考虑封装成mixin,不过请写好注释哦。不然就会有人在背后骂你了!!你懂的~~
7. 优化
7.1 gzip压缩
- 安装compression-webpack-plugin插件
npm install compression-webpack-plugin --save-dev // or yarn add compression-webpack-plugin --dev 复制代码
- 在 vue.config.js 中添加配置
// vue.config.js const CompressionPlugin = require('compression-webpack-plugin') module.exports = { chainWebpack: config => { // 这里是对环境的配置,不同环境对应不同的BASE_URL,以便axios的请求地址不同 config.plugin('define').tap(args => { args[0]['process.env'].BASE_URL = JSON.stringify(process.env.BASE_URL) return args }) if (process.env.NODE_ENV === 'production') { // #region 启用GZip压缩 config .plugin('compression') .use(CompressionPlugin, { asset: '[path].gz[query]', algorithm: 'gzip', test: new RegExp('\\.(' + ['js', 'css'].join('|') + ')