写在最前面
前端要封装一套顺手的开发脚手架,一个是提高开发效率,一个是公司中每个前端搭建的风格都不一样,项目比较难维护,所以就搞了这么一个东西,主要集成了以下内容:
- vue(2.6.10)
- vuex
- vue-router
- typescript
- sass
- 移动端界面适配,自动转为vw
- 引入并封装了axios
- 按需引入vant
- husky + @commitlint/cli 规范化提交
- ESlint
- 优化了打包体积
- 优化了构建速度
项目地址在这:github,如果有写的不对的地方欢迎指出;如果觉得还行,能否给个star
,感谢~
检查环境
在搭建之前检查一下vue-cli
的版本(vue --version
)和node
是否安装(node --version
):
-
vue
:3.0.4 -
node
:v14.15.0
如果怕搭建环境的过程中网速影响安装,可以执行下列命令:
yarn config set registry https://registry.npm.taobao.org/
脚手架安装
vue create vue2-mobild-cli
不采用默认配置项,选择: Manually select features
这里我们要配置babel
、TyperScript
、Router
、Vuex
、CSS Pre-processors
(css预处理)、Linter Formatter
(代码风格、格式校验)、Unit Testing
(单元测试)和E2E Testing
(端对端测试)。
具体选择如下:
-
Use class-style component syntax?
:保持使用TypeScript
的class
风格; -
Use Babel alongside TypeScript?
:使用Babel
与TypeScript
一起用于自动检测的填充; -
Use history mode for router?
: 路由模式选择hash
; -
Pick a CSS pre-processor
:CSS
预加载器我选择使用Sass
; -
Linter / Formatter
: 这里不采用TSLint
,TSLint
的团队已经停止维护,且建议用ESLint
的,这里使用Airbnb
的规范; -
Pick additional lint features
:校验的时间选择保存的时候,这样可以即时更正嘛; -
Pick a unit testing solution:
单元测试解决方案跟随主流Mocha + chai
; -
Pick a E2E testing solution:
端对端测试类型Cypress
。
看看安装之后运行是否成功。
提交规范
yarn add --dev husky @commitlint/config-conventional @commitlint/cli
husky 的作用就是在提交的时候,会通过读取husky
的配置中的hooks
(可以理解成GIT生命钩子)自动触发某些动作。
@commitlint/config-conventional和@commitlint/cli校验commit
是否符合提交规范,常见的提交类型有:
[
'chore', // 构建过程或辅助工具的变动
'docs', // 文档
'feat', // 新功能
'fix', // 修补bug
'refactor', // 重构(即不是新增功能,也不是修改bug的代码变动)
'revert',
'style', // 格式(不影响代码运行的变动)
'test' // 增加测试
];
新建commitlint.config.js
:
module.exports = {extends: ['@commitlint/config-conventional']};
配置package.json
:
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
然后测试一下:
git add .
git commit -m "add git-lint" ## 会提示你不规范
git commit -m "feat: add git-lint" ## 提交成功
搭配ESlint
,确保每次提交代码前走一遍ESLint
,修改package.json
:
"lint": "eslint --ext .js --ext .vue --ext .ts src",
"lint:fix": "vue-cli-service lint",
lint
是检测代码是否符合规范;lint:fix
是自动规范化代码。
"husky": {
"hooks": {
"pre-commit": "npm run lint",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
然后在任意一个文件中删除分号(因为现在ESLint
默认是强制分号的),执行:
git add .
git commit -m "feat: test ESLint" ## 会报错
npm run lint:fix
git commit -m "feat: test ESLint" ## 提交成功
如果提交的时候看不惯这种报错:
CRLF will be replaced by LF in xxxx
可以配置:
git config --global core.autocrlf true
整理路由
我喜欢按模块分路由,如果现在项目中有个模块是笔记,我们可以新增笔记、查看笔记列表和详情,我们修改views
目录,新增以下文件:
每个文件大致是这样的内容:
<!--
作者:Armouy
创建时间:2021年01月25日 16:11:08
备注:新增笔记
-->
<template>
<div>新增笔记</div>
</template>
<script lang='ts'>
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
export default class AddNote extends Vue {}
</script>
<style scoped lang='scss'>
</style>
修改App.vue
:
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<router-link to="/note/add">AddNote</router-link> |
<router-link to="/note/list">NoteList</router-link>
</div>
<router-view/>
</div>
</template>
然后删除router.ts
,新建router
文件夹:
是的,没错,以后每新增一个模块,我们就可以根据这个模块新增一个路由文件,这样就不用所有路由都放在一个文件中了:
// note.ts
import Router, { RouteConfig } from 'vue-router';
const R = (name: string) => () => import(`@/views/note/${name}.vue`);
const RoutesConfig: RouteConfig[] = [{
name: 'AddNote',
path: '/note/add',
component: R('Add'),
meta: {
title: '新增笔记',
},
}, {
name: 'NoteList',
path: '/note/list',
component: R('List'),
meta: {
title: '笔记列表',
},
}, {
name: 'NoteDetails',
path: '/note/details',
component: R('Details'),
meta: {
title: '笔记详情',
},
}];
export default RoutesConfig;
// index.ts
import Vue from 'vue';
import Router, { RouteConfig } from 'vue-router';
import note from './note';
const R = (name: string) => () => import(`@/views/${name}.vue`);
Vue.use(Router);
const RoutesConfig: RouteConfig[] = [
...note,
{
name: 'Home',
path: '/',
component: R('Home'),
meta: {
title: '首页',
},
}, {
name: 'About',
path: '/about',
component: R('About'),
meta: {
title: '关于',
},
}, {
name: 'NotFound',
path: '*',
component: R('NotFound'),
meta: {
title: '404',
},
},
];
const createRouter = () => new Router({
mode: 'hash',
routes: RoutesConfig,
});
const router = createRouter();
export default router;
如果根据http://localhost:8080/#/note/add
可以成功访问路径,就说明成功了。然后我们来补充一下index.ts
的细节,加入三个钩子:
// 在路由跳转前触发,可以拦截未登录的情况
router.beforeEach((to: any, from: any, next: () => void): void => {
const title = to.meta && to.meta.title ? to.meta.title : '首页';
document.title = title;
// check login, so that ...
next();
});
// 路由跳转后触发
router.afterEach((to: any, from: any): void => {
// do something ...
});
// 跳转报错的情况
router.onError((error: any) => {
// do something ...
});
最后,在App.vue
上加上keep-alive
,进行缓存:
<keep-alive>
<router-view/>
</keep-alive>
配置样式
在assets
中新建如下文件:
-
var.scss
:主要的变量,如颜色变量、字号变量等; -
reset.css
: 初始化样式; -
common.scss
: 公用样式。
在main.ts
中引入reset.css
和common.scss
:
import './assets/style/reset.scss';
import './assets/style/common.scss';
全局变量的使用需要配置vue.config.js
(还有一种方法就是还可以搭配使用style-resources-loader
):
// vue.config.js
const path = require('path');
module.exports = {
css: {
loaderOptions: {
scss: {
prependData: `@import "~@/assets/style/var.scss";`
}
}
}
}
在var.scss
中加入变量:
$primaryColor: rgba(41, 113, 255, 1);
然后在App.vue
中测试一下:
color: $primaryColor;
颜色显示则成功。
界面适配
可以参考这篇文章,移动端布局之postcss-px-to-viewport。
yarn add --dev postcss-px-to-viewport
新增.postcssrc.js
:
module.exports = {
plugins: {
autoprefixer: {}, // 用来给不同的浏览器自动添加相应前缀,如-webkit-,-moz-等等
"postcss-px-to-viewport": {
unitToConvert: "px", // 要转化的单位
viewportWidth: 750, // UI设计稿的宽度
unitPrecision: 6, // 转换后的精度,即小数点位数
propList: ["*"], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
viewportUnit: "vw", // 指定需要转换成的视窗单位,默认vw
fontViewportUnit: "vw", // 指定字体需要转换成的视窗单位,默认vw
selectorBlackList: ["wrap"], // 指定不转换为视窗单位的类名,
minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
replace: true, // 是否转换后直接更换属性值
exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
landscape: false // 是否处理横屏情况
}
}
};
运行之后会发现这段代码:
.home{
height: 100px;
font-size: 40px;
text-align: center;
line-height: 100px;
}
会被转为:
.home[data-v-fae5bece] {
height: 13.333333vw;
font-size: 5.333333vw;
text-align: center;
line-height: 13.333333vw;
}
引入vant
yarn add vant
yarn add --dev babel-plugin-import
修改babel.config.js
:
module.exports = {
presets: [
'@vue/app',
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
};
测试一下,在main.ts
中使用:
import { Button } from 'vant';
Vue.use(Button);
在Home.vue
中使用:
<van-button type="primary">主要按钮</van-button>
能正常显示即成功。
为了代码好维护,在根目录新建plugins/vant
:
import Vue from 'vue';
import { Button } from 'vant';
Vue.use(Button);
修改main.ts
:
import './plugins/vant';
引入axios
yarn add axios
yarn add qs
yarn add --dev @types/qs
配置拦截器
在src/utils
下新建axios.ts
,设置一下拦截器等内容:
import axios, { AxiosRequestConfig } from 'axios';
const instance: AxiosRequestConfig | any = axios.create({
timeout: 100000,
headers: {
post: {
'Content-Type': 'application/x-www-form-urlencoded',
},
},
withCredentials: true,
});
instance.interceptors.request.use((config: AxiosRequestConfig) => {
const token = localStorage.getItem('token');
if (token && token !== '') {
config.headers.Authorization = token;
}
console.log('config', config);
return config;
});
instance.interceptors.response.use(
(res: any) => res.data,
(error: any) => {
console.log('%c 接口异常 ', 'background-color:orange;color: #FFF;border-radius: 4px;', error);
// 可以根据状态码做其他操作:登录过期时、网络请求不存在时...
},
);
export default instance;
在src/api
下新建config.ts
、note.ts
和index.ts
:
// config.ts
// 一个项目可能会请求多个域名
const baseUrl: {[key: string]: any} = {
crm: '/api',
loan: '/loan',
};
export default baseUrl;
路由也是分模块的,我们把关于note
的相关路由都写在note.ts
中:
const note = {
};
export default note;
最后是index.ts
:
import note from './note';
export default {
note,
};
挂载全局
为了方便调用,我们将axios
请求挂载在全局:
// main.ts
import api from '@/api';
Vue.prototype.$api = api;
声明
如果我们直接使用this.$api
会报错,我们需要改一下ts
声明文件:
// shims-vue.d.ts
import Vue from 'vue';
// 对vue进行类型补充说明
declare module 'vue/types/vue' {
interface Vue {
$api: any
}
}
然后再新建一个shims.d.ts
文件:
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
我们修改了shims-vue.d.ts
,此时main.ts
中的import App from './App.vue';
会提示找不到这个模块,所以才新建了shimx.d.ts
。(其实这个文件的内容就是原先shims-vue.d.ts
中 的内容,但是两个写在一起时import App from './App.vue';
会提示找不到这个模块)。
get请求
我们先实现一个get请求
,获得列表:
import axios from '@/utils/axios';
import baseUrl from './config';
const note = {
getList(){
return axios.get(`${baseUrl.crm}/note/all`);
},
};
export default note;
然后我们在home.vue
中点击按钮只会调用一下:
private async handleClick(){
this.$api.note.getList().then((res: any) => {
console.log('res', res);
});
}
这时候会报跨域的问题需要配置一下:
// vue.config.js
devServer: {
hot: true,
proxy: {
'/api': {
target: 'http://xxx.com/api',
changeOrigin: true,
ws: true,
pathRewrite: {
'^/api': '',
},
},
},
},
即我们请求api/note/all
时会变为http://xxx.com/api/note/all
。(这就是为啥上面我们的baseUrl
中写的是没有域名的url
前缀了)。
post请求
接下来是post
请求,这里我们要配合一下qs
这个库:
import axios from '@/utils/axios';
import qs from 'qs';
import baseUrl from './config';
const note = {
getList(){
return axios.get(`${baseUrl.crm}/note/all`);
},
add(params: any){
return axios.post(`${baseUrl.crm}/note/add`, qs.stringify(params));
}
};
export default note;
private async handleClick(){
await this.$api.note.add({
name: '客户姓名',
mobile: '13536548511',
}).then((res: any) => {
console.log(res);
});
}
需要考虑一下使用FormData上传文件的情况:
import axios from '@/utils/axios';
import qs from 'qs';
import baseUrl from './config';
const note = {
getList(){
return axios.get(`${baseUrl.crm}/note/all`);
},
add(params: any){
return axios.post(`${baseUrl.crm}/note/add`, qs.stringify(params));
},
upload(params: any){
return axios.request({
url: `${baseUrl.crm}/note/upload`,
method: 'post',
headers: { 'Content-Type': 'multipart/form-data' },
data: params,
});
},
};
export default note;
private async handleClick(){
const param = new FormData();
param.append('file', this.file);
await this.$api.note.upload(param);
}
最后我们来区分一下生产环境和测试环境,修改一下config.ts
:
const env = process.env.NODE_ENV;
let crmUrl;
let loanUrl;
switch (env){
case 'development':
crmUrl = '/api';
loanUrl = '/loan';
break;
case 'production':
crmUrl = 'http://aaa.com/api';
loanUrl = 'https://bbb.com/loan';
break;
default:
crmUrl = '/api';
loanUrl = '/loan';
break;
}
const baseUrl: {[key: string]: any} = {
crm: crmUrl,
loan: loanUrl,
};
export default baseUrl;
到这里axios
的配置就差不多了。
使用vuex
yarn add vuex-module-decorators vuex-class
yarn add vuex-class
vuex
依旧采用的是分模块的思想,删除原先的store.ts
,在根目录新建store
文件夹:
在note.ts
中我们来实现state、mutation和action
的写法:
import api from '@/api';
import {
VuexModule, Module, Mutation, Action,
} from 'vuex-module-decorators';
@Module({ namespaced: true })
export default class NoteModule extends VuexModule {
public noteList: any[] = [];
@Mutation
public initNote(noteList: any[]){
this.noteList = noteList;
}
@Action({ commit: 'initNote' })
async initNoteList(){
const res = await api.note.getList();
return res.data ? res.data : [];
}
}
在store/index.ts
中使用:
import Vue from 'vue';
import Vuex from 'vuex';
import NoteModule from './modules/note';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
Note: NoteModule,
// 其他模块...
},
});
如何使用呢?回到home.vue
中:
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { namespace } from 'vuex-class';
const NoteModule = namespace('Note');
@Component
export default class Home extends Vue {
@NoteModule.State('noteList') public noteList: any[] | undefined;
@NoteModule.Action('initNoteList') public initNoteList: any;
private async handleClick(){
// ...
}
private async handleClickGetVuex(){
console.log(this.noteList);
await this.initNoteList();
console.log(this.noteList);
}
}
</script>
优化打包和构建速度
最主要的配置打包压缩,代码经常重构优化也有助于体积减少哦,其他常见的优化可以查看我之前写的 Vue项目优化小结和 Vue项目优化小结(二)。
yarn add --dev speed-measure-webpack-plugin compression-webpack-plugin hard-source-webpack-plugin
const path = require('path');
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
const productionGzipExtensions = ['js', 'css'];
const resolve = dir => path.join(__dirname, dir);
module.exports = {
// 是否在构建生产包时生成 sourceMap 文件,false将提高构建速度
productionSourceMap: false,
publicPath: './',
css: {
loaderOptions: {
scss: {
prependData: '@import "~@/assets/style/var.scss";',
},
},
},
configureWebpack: () => {
const plugins = [];
plugins.push(new SpeedMeasurePlugin());
if (process.env.NODE_ENV === "'production'"){
plugins.push(new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: productionGzipExtensions,
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false, // 是否删除源文件
}));
} else {
plugins.push(new HardSourceWebpackPlugin());
}
return {
output: {
filename: 'js/[name].js',
chunkFilename: 'js/[name].js',
},
plugins,
};
},
devServer: {
hot: true,
proxy: {
'/api': {
target: 'http://crmtest.8kqw.com/api',
changeOrigin: true,
ws: true,
pathRewrite: {
'^/api': '',
},
},
},
},
};
最后
附上github地址: vue2-mobile-cli。
如有不对欢迎指出与交流,感谢阅读~