vue3-h5-template

Based on Vue3+TypeScript+ VUUE-CLI4.0 + Vant UI + Sass + REM adaptation scheme + AXIOS packaging + JSSDK configuration + VConsole mobile debugging, the mobile end template scaffolding is constructed

Project address: Github

You are advised to view demo on a mobile phone

Node Version Requirements

Vue CLI requires Node.js version 8.9 or later (8.11.0+ recommended). You can use NVM or NVM-Windows to manage multiple versions of Node on the same computer.

This example node.js 12.14.0

The project structure

Voe-h5-template -- UI Home Directory ├─ Public -- Static Resources ├─ FavIcon ├─ index.html -- Home ├─ SRC -- Source Directory ├─ API ├─ Assets - Static Resources Directory ├─ CSS ├ ├─ Index. SCSS - Everything ├ ├─ Mixin. SCSS - Everything Mixin ├ ├─ Variables. SCSS - Global Variables ├ exercises - Components - Package Component ├ Exercises - Config - Environment Configuration ├ Exercises - Hooks - Vue3 Hooks ├ Exercises - Model - Type Statement File ├ Exercises ├ ─ ─const│ ├── Route │ ├── Route │ ├── Route │ ├── Route │ ├── Router Store -- VUEX ├ exercises ── Utils -- Tool Kit ├ Exercises ── Request.js -- Axios ├ Exercises ─ Storage.js -- Local Store ├ Exercises ── Views -- Business Vue page ├ ├ ├ ─ ─ layouts, route layout page (whether the cached page) ├ ├ ├ ─ ─ tabBar - menu at the bottom of the page ├ ├ └ ─ ─ orther - other pages ├ ├ ─ ─ App. Vue - the root component ├ ├ ─ ─ main. Ts - Entrance ts ├ ├ ─ ─ shims - axios. Which s - axios declaration file ├ └ ─ ─ shims - vue. Which s - vue component declaration file ├ ─ ─ the env. The development, the development environment ├ ─ ─ Env. Production - Production ├─.env.staging - Test Environment ├─.eslintrc.js - ESLint Configure ├─.gitignore - git Ignore ├─ .postCSRC. Js - CSS Pre-Processing (REM Adaptation) ├─ Babel.config. js - Barbel Configuration entry ├─ tsconfig.json - VSCode Path Import Configuration ├─ Package. json -- Dependency Management ├ ─ vue.config.js -- Webpack configuration for vue Cli4Copy the code

Start the project


git clone https://github.com/ynzy/vue3-h5-template.git

cd vue3-h5-template

npm install

npm run serve
Copy the code

directory

  • √ Configure multiple environment variables
  • √ REM adaptation
  • √ Load VantUI components as required
  • √Sass global style
  • √ Fit apple bottom safe distance
  • √ Use Mock data
  • √Axios encapsulation and interface management
  • √Vuex status management
  • Tick the Vue – the router
  • √Webpack 4 Vue.config. js Basic configuration
  • √ Configure the alias
  • √ Configure proxy across domains
  • √ Configure package analysis
  • √ Externals Imports CDN resources
  • Tick off the console. The log
  • √splitChunks package third-party modules separately
  • Square root gzip compression
  • Square root uglifyjs compression
  • √ VConsole Mobile debugging
  • √ Dynamically set title
  • √ Local storage encapsulation
  • Square root configuration Jssdk
  • √Eslint + Pettier Unified development specification

✅ Configure multiple environment variables

The scripts in package.json are configured to serve stage builds, using –mode XXX to execute different environments

  • throughnpm run serveStart the local PC and rundevelopment
  • throughnpm run stageStart the test and executedevelopment
  • throughnpm run prodStart development, executedevelopment
  • throughnpm run stageBuildPackage the tests and execute themstaging
  • throughnpm run buildPackage formally, executeproduction
"scripts": {
  "serve": "vue-cli-service serve --open"."stage": "cross-env NODE_ENV=dev vue-cli-service serve --mode staging"."prod": "cross-env NODE_ENV=dev vue-cli-service serve --mode production"."stageBuild": "vue-cli-service build --mode staging"."build": "vue-cli-service build",}Copy the code
Configuration is introduced

Variables that begin with VUE_APP_ are accessible in code through process.env.vue_app_. For example,VUE_APP_ENV = ‘development’ is accessed through process.env.vue_app_env. In addition to the VUE_APP_* variable, two special variables NODE_ENV and BASE_URL are always available in your application code to create.env.* in the project root directory

  • .env.development Local development environment configuration
NODE_ENV='development'
# must start with VUE_APP_
VUE_APP_ENV = 'development'

Copy the code
  • Env.staging test environment configuration
NODE_ENV='production'
# must start with VUE_APP_
VUE_APP_ENV = 'staging'
Copy the code
  • .env.production Specifies the environment configuration
 NODE_ENV='production'
# must start with VUE_APP_
VUE_APP_ENV = 'production'
Copy the code

There are not many variables defined here, just the basic VUE_APP_ENV development staging Production variables which we manage in SRC /config/env.*.ts.

There is a problem here, since there is a file for setting variables for different environments, why go to config and create three corresponding files? Easy to modify, do not need to restart the project, in line with the development habit.

config/index.js

exportinterface IConfig { env? : string// Development environmenttitle? : string/ / project titlebaseUrl? : string// Project addressbaseApi? : string// the API requests the addressAPPID? : string// Public appId is usually stored on the server sideAPPSECRET? : string// The public account appScript is stored on the server side
	$cdn: string // CDN public resource path
}

// Introduce different configurations according to the environment process.env.node_env
const config = require('./env.' + process.env.VUE_APP_ENV)
module.exports = config
Copy the code

And defines the interface type, convenient when we can automatically identify the parameters

Configure environment variables. The local environment file env.develop. js is used as an example. You can modify the variables as required

// Local environment configuration
module.exports = {
	title: 'vue-h5-template'.baseUrl: 'http://localhost:9018'.// Project address
	baseApi: 'https://test.xxx.com/api'.// The local API requests the address
	APPID: 'xxx'.APPSECRET: 'xxx'
}
Copy the code
Call the config
import config from '@/config/index'
setup() {
  console.log('Environment Configuration', config)
}
Copy the code

Bring back to the top

✅ REM adaptation scheme

Don’t worry, rem adaptation has been configured for the project. Here’s a brief introduction:

By default, styles in Vant use px units. If you want to use REM units, the following two tools are recommended:

  • postcss-pxtoremIs apostcssPlug-in for converting units torem
  • amfe-flexibleUsed to set theremAt baseline
yarn add postcss-pxtorem --dev
yarn add amfe-flexible --save
Copy the code
PostCSS configuration

The following provides a basic PostCSS configuration that can be modified based on project requirements

// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
	plugins: {
		autoprefixer: {
			overrideBrowserslist: ['the Android 4.1'.'iOS 7.1'.'Chrome > 31'.'ff > 31'.'ie >= 8']},'postcss-pxtorem': {
			rootValue: 37.5.propList: [The '*']}}}Copy the code

I used amFE-flexible to set rem, see Github says this is better, which one to use for reference

// main.ts
// Mobile adapter
import 'amfe-flexible'
Copy the code

More details: Vant

Beginners must see, old birds skip

A lot of people ask me about fit.

We know that 1rem is equal to the font-size PX value of the HTML root element. Vant UI set rootValue: 37.5, which you can see on iPhone 6 (1rem equals 37.5px) :

<html data-dpr="1" style="The font - size: 37.5 px;"></html>
Copy the code

The root element may have a different font size when switching between different models. When you write CSS PX, the program converts it to REM to match.

Since we are using Vant components, we need to write the style as rootValue: 37.5.

For example: the design gives you a 750px by 1334px image that covers the screen on the iPhone6 and other models.

  • whenrootValue: 70, the stylewidth: 750px; height: 1334px;The picture will fill up the iPhone6 screen, and when you switch to another model, the picture will fill up as well.
  • whenRootValue: 37.5When stylewidth: 375px; height: 667px;Images will fill the iPhone6 screen.

That’s 375px CSS for iPhone 6. Other you can according to your design, to write the corresponding style can be.

Of course, you can use 100% if you want to fill the screen, but this is just an example.

<img class="image" src="https://imgs.solui.cn/weapp/logo.png" />

<style>
	/* rootValue: 75 */
	.image {
		width: 750px;
		height: 1334px;
	}
	/ * rootValue: 37.5 * /
	.image {
		width: 375px;
		height: 667px;
	}
</style>
Copy the code

Bring back to the top

✅ VantUI components are loaded on demand

The project adopts Vant to automatically introduce components on demand (recommended)

Generally speaking, TS uses plan 2, but I have some problems in the process of using it, so I use Plan 1

Solution a:

Babel-plugin-import is a Babel plug-in that automatically converts the way import is written to import on demand during compilation

Installing a plug-in

npm i babel-plugin-import -D
Copy the code

In the Babel. Config. Js Settings

// For users using babel7, this can be configured in babel.config.js
const plugins = [
	[
		'import',
		{
			libraryName: 'vant'.libraryDirectory: 'es'.style: true
		},
		'vant']]module.exports = {
	presets: [['@vue/cli-plugin-babel/preset', { useBuiltIns: 'usage'.corejs: 3 }]],
	plugins
}
Copy the code

Ts-import-plugin is a modular import plugin for TypeScript

Yarn add ts-import-plugin –dev then add it in vue. Config.js

const merge = require('webpack-merge')
const tsImportPluginFactory = require('ts-import-plugin')
// * Implementation of tripartite UI loading on demand under TS
const mergeConfig = config= > {
	config.module
		.rule('ts')
		.use('ts-loader')
		.tap(options= > {
			options = merge(options, {
				transpileOnly: true.getCustomTransformers: () = > ({
					before: [
						tsImportPluginFactory({
							libraryName: 'vant'.libraryDirectory: 'es'.style: true}})]),compilerOptions: {
					module: 'es2015'}})return options
		})
}
Copy the code

Using the component

The project manages components in a unified way under SRC /plugins/ ant

// Globally import vant components as needed
import { App as VM } from 'vue'
import { Button, Cell, CellGroup, Icon } from 'vant'

const plugins = [Button, Icon, Cell, CellGroup]

export const vantPlugins = {
	install: function(vm: VM) {
		plugins.forEach(item= > {
			vm.component(item.name, item)
		})
	}
}
Copy the code

Bring back to the top

✅ Sass global style

Dart-sass ensures fast installation and high probability of installation failure

Each page’s own style is written in its own.vue file scoped adds the concept of a domain to CSS as its name suggests.

<style lang="scss">
	/* global styles */
</style>

<style lang="scss" scoped>
	/* local styles */
</style>
Copy the code

The directory structure

Vue-h5-template All global styles are set in the @/ SRC /assets/ CSS directory

├── Assets │ ├─ CSS │ ├─ Index# Global universal style│ │ ├ ─ ─ reset. SCSSClear the browser default style│ │ ├ ─ ─ a mixin. SCSS# global mixin│ │ └ ─ ─ the variables. SCSS# global variables
Copy the code

Vue.config. js adds global style configuration

css: {
	loaderOptions: {
		scss: {
			// Pass the shared global variable to the global SASS style. $SRC can be configured with the image CDN prefix
			/ / details: https://cli.vuejs.org/guide/css.html#passing-options-to-pre-processor-loaders
			prependData: `
				@import "assets/css/mixin.scss";
				@import "assets/css/variables.scss";
				`
			// $cdn: "${defaultSettings.$cdn}";}}},Copy the code

$CDN can be accessed in.vue files using this.$CDN

// Introduce global styles
import '@/assets/css/index.scss'

// set js to access $CDN
/ / into the CDN
import { $cdn } from '@/config'
Vue.prototype.$cdn = $cdn
Copy the code

Use in CSS and JS

<script>
	console.log(this.$cdn)
</script>
<style lang="scss" scoped>
	.logo {
		width: 120px;
		height: 120px;
		background: url($cdn+'/weapp/logo.png') center / contain no-repeat;
	}
</style>
Copy the code

Bring back to the top

Custom Vant-UI styles

Now let’s talk about how to rewrite the vuant-UI style. Since the styles of the Ant-UI are introduced globally, you can’t add scoped if you want to cover only the vant styles of the page. If you want to cover only the Vant styles of the page, you can add a class to its parent and use the namespace to solve the problem.

.about-container {
	/* Your namespace */
	.van-button {
		/* Vant-ui element */
		margin-right: 0px; }}Copy the code

The parent component changes the child component style depth selector

When you want to style a child component using scoped in the parent component you can do this by using ::v-deep:

<style scoped>
::v-deep .a {
	.b { /* ... */ }
}
</style>
Copy the code

Bring back to the top

✅ for Apple bottom safe distance

The meta of index.html specifies viewport-fit=cover

The bottom safe distance parameter comes with vant

<! Add meta tag to head tag and set viewport-fit=cover --><meta
  name="viewport"
  content="Width =device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover"
/><! -- Enable the top safety zone adaptation --><van-nav-bar safe-area-inset-top /><! -- Enable bottom safety zone adaptation --><van-number-keyboard safe-area-inset-bottom />
Copy the code

If you don’t use an adaptation in Vant, you can write it yourself. I wrote a generic style in SCSS

.fixIphonex {
	padding-bottom: $safe-bottom ! important;
	&::after {
		content: ' ';
		position: fixed;
		bottom: 0 ! important;
		left: 0;
		height: calc(#{$safe-bottom} + 1px);
		width: 100%;
		background: #ffffff; }}Copy the code

Bring back to the top

✅ Use Mock data

Mock requests are wrapped with vue-elemental-admin mock requests, and can be used directly

  • mock.js
const Mock = require('mockjs')

const user = require('./user')
// const role = require('./role')
// const article = require('./article')
// const search = require('./remote-search')

// const mocks = [...user, ...role, ...article, ...search]
const mocks = [...user]
// for front mock
// please use it cautiously, it will redefine XMLHttpRequest,
// which will cause many of your third-party libraries to be invalidated(like progress event).
function mockXHR() {
	// mock patch
	// https://github.com/nuysoft/Mock/issues/300
	Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
	Mock.XHR.prototype.send = function() {
		if (this.custom.xhr) {
			this.custom.xhr.withCredentials = this.withCredentials || false

			if (this.responseType) {
				this.custom.xhr.responseType = this.responseType
			}
		}
		this.proxy_send(... arguments) }function XHR2ExpressReqWrap(respond) {
		return function(options) {
			let result = null
			if (respond instanceof Function) {
				const { body, type, url } = options
				// https://expressjs.com/en/4x/api.html#req
				result = respond({
					method: type,
					body: JSON.parse(body),
					query: url
				})
			} else {
				result = respond
			}
			return Mock.mock(result)
		}
	}

	for (const i of mocks) {
		Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
	}
}

module.exports = {
	mocks,
	mockXHR
}
Copy the code
  • user.js
const tokens = {
	admin: {
		token: 'admin-token'
	},
	editor: {
		token: 'editor-token'}}const users = {
	'admin-token': {
		roles: ['admin'].introduction: 'I am a super administrator'.avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif'.name: 'Super Admin'
	},
	'editor-token': {
		roles: ['editor'].introduction: 'I am an editor'.avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif'.name: 'Normal Editor'}}module.exports = [
	// user login
	{
		url: '/vue-h5/user/login'.type: 'post'.response: config= > {
			const { username } = config.body
			const token = tokens[username]

			// mock error
			// if (! token) {
			// return {
			// code: 60204,
			// message: 'Account and password are incorrect.'
			/ /}
			// }

			return {
				code: 20000.data: token,
				msg: 'Login successful'}}},// get user info
	{
		url: '/vue-h5/user/info.*'.type: 'get'.response: config= > {
			const { token } = config.query
			const info = users['admin-token']
			// mock error
			// if (! info) {
			// return {
			// code: 50008,
			// message: 'Login failed, unable to get user details.'
			/ /}
			// }

			return {
				code: 20000.data: info,
				msg: 'Login successful'}}},// user logout
	{
		url: '/vue-h5/user/logout'.type: 'post'.response: _= > {
			return {
				code: 20000.data: 'success'}}}]Copy the code
  • If you don’t need main.js, just get rid of this code
// Use mock data
if (config.mock) {
	const { mockXHR } = require('.. /mock')
	mockXHR()
}
Copy the code
  • Interface request
onMounted(() = > {
	axios
		.get('/vue-h5/user/info')
		.then(res= > {
			console.log(res)
		})
		.catch(err= > {
			console.error(err)
		})
})
Copy the code

✅ Axios encapsulation and interface management

Utils /request.js encapsulates AXIOS and developers need to make changes based on the background interface.

  • service.interceptors.request.useRequest headers can be set, such as Settingstoken
  • config.hideloadingIt is set in the interface parameters in the API folder, as described below
  • service.interceptors.response.useIn the interface can return data processing, such as 401 delete local information, login again
/ * * *@description [axios requests encapsulation] */
import store from '@/store'
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'
// import {Message, Modal} from 'view-design' // UI component library
import { Dialog, Toast } from 'vant'
import router from '@/router'
// Introduce different API addresses according to the environment
import config from '@/config'

const service = axios.create({
	baseURL: config.baseApi + '/vue-h5'.// url = base url + request url
	timeout: 5000.withCredentials: false // send cookies when cross-domain requests
	// headers: {
	// // clear cors
	// 'Cache-Control': 'no-cache',
	// Pragma: 'no-cache'
	// }
})

// Request interceptors
service.interceptors.request.use(
	(config: AxiosRequestConfig) = > {
		// Load the animation
		if (config.loading) {
			Toast.loading({
				message: 'Loading... '.forbidClick: true})}// Add request headers here, such as token
		// if (store.state.token) {
		// config.headers['Authorization'] = `Bearer ${store.state.token}`
		// }
		return config
	},
	(error: any) = > {
		Promise.reject(error)
	}
)

// Response interceptors
service.interceptors.response.use(
	async (response: AxiosResponse) => {
		// await new Promise(resovle => setTimeout(resovle, 3000))
		Toast.clear()
		const res = response.data
		if(res.code ! = =0) {
			/ / token expired
			if (res.code === 401) {
				// Warning window
				return
			}
			if (res.code == 403) {
				Dialog.alert({
					title: 'warning'.message: res.msg
				}).then(() = > {})
				return
			}
			// If the background returns an error value, the corresponding error object is returned here, and the following error is received
			return Promise.reject(new Error(res.msg || 'Error'))}else {
			// Note the return value
			return response.data
		}
	},
	(error: any) = > {
		Toast.clear()
		if (error && error.response) {
			switch (error.response.status) {
				case 400:
					error.message = 'Request error (400)'
					break
				case 401:
					error.message = 'Not authorized, please log in to (401)'
					break
				case 403:
					error.message = 'Access denied (403)'
					break
				case 404:
					error.message = 'Error requesting address:${error.response.config.url}`
					break
				case 405:
					error.message = 'Requested method not allowed (405)'
					break
				case 408:
					error.message = 'Request timed out (408)'
					break
				case 500:
					error.message = 'Server internal error (500)'
					break
				case 501:
					error.message = 'Service Not realized (501)'
					break
				case 502:
					error.message = 'Network Error (502)'
					break
				case 503:
					error.message = 'Service unavailable (503)'
					break
				case 504:
					error.message = 'Network Timeout (504)'
					break
				case 505:
					error.message = 'HTTP version not supported (505)'
					break
				default:
					error.message = 'Connection error:${error.message}`}}else {
			if (error.message == 'Network Error') {
				error.message == 'Network exception, please check and try again! '
			}
			error.message = 'Failed to connect to server, please contact administrator'
		}
		Toast(error.message)
		// store.auth.clearAuth()
		store.dispatch('clearAuth')
		return Promise.reject(error)
	}
)

export default service
Copy the code

Interface management

Unify the management interface in the SRC/API folder

  • You can set up multiple module docking interfaces, such ashome.tsHere is the interface of the home pageauthController.ts
  • urlInterface address, which will be concatenated when requestedconfigUnder thebaseApi
  • methodRequest method
  • dataRequest parametersqs.stringify(params)Is the data serialization operation
  • loadingThe defaultfalse, is set totrueSome interfaces in the loading UI interaction need to be perceived by the user
import request from '@/utils/request'
export interface IResponseType<P = {}> {
	code: number
	msg: string
	data: P
}
interface IUserInfo {
	id: string
	avator: string
}
interface IError {
	code: string
}
export const fetchUserInfo = () = > {
	return request<IResponseType<IUserInfo>>({
		url: '/user/info'.method: 'get'.loading: true})}Copy the code

How to call

Because the awaitWrap type derivation is cumbersome, try catch is used to catch errors, both interface errors and business logic errors

onMounted(async() = > {try {
		let res = await fetchUserInfo()
		console.log(res)
	} catch (error) {
		console.log(error)
	}
})
Copy the code

Bring back to the top

✅ Vuex status management

The directory structure

├ ─ ─ store │ ├ ─ ─ modules │ ├ ─ ─ | ─ ─ Auth │ ├ ─ ─ ├ ─ ─ ├ ─ ─ but ts │ ├ ─ ─ ├ ─ ─ ├ ─ ─ interface. The ts │ ├ ─ ─ ├ ─ ─ └ ─ ─ types. The ts │ ├ ─ ─ Index. Ts │ ├ ─ ─ getters. TsCopy the code

The type definition

  • Module type

interface.ts

import { IUserInfo } from '@/api/interface'

/** * User information */
export interface IAuthState {
	userInfo: IUserInfo
}
Copy the code

index.ts

import { Module } from 'vuex'
import { IGlobalState } from '@/store/index'
import { IAuthState } from '@/store/modules/Auth/interface'
import * as Types from '@/store/modules/Auth/types'

const state: IAuthState = {
	userInfo: {}}const login: Module<IAuthState, IGlobalState> = {
	namespaced: true,
	state,
	mutations: {
		[Types.SAVE_USER_INFO](state, data) {
			state.userInfo = data
		}
	},
	actions: {
		async [Types.SAVE_USER_INFO]({ commit }, data) {
			return commit(Types.SAVE_USER_INFO, data)
		}
	}
}

export default login
Copy the code
  • Global Store type

Import the module type into index.ts to define the global type

import { IAuthState } from './modules/Auth/interface'

export interface IGlobalState {
	auth: IAuthState
}

const store = createStore<IGlobalState>({
	getters,
	modules: {
		auth
	}
})

export default store
Copy the code

The main ts introduced

import { createApp } from 'vue'
import store from './store'

const app = createApp(App)
app.use(store)
app.mount('#app')
Copy the code

use

import { fetchUserInfo } from '@/api/authController.ts'
import { useStore } from 'vuex'
import * as Types from '@/store/modules/Auth/types'
import { IGlobalState } from '@/store'

export default defineComponent({
	name: 'about'.props: {},
	setup(props) {
		const store = useStore<IGlobalState>()
		const userInfo = computed(() = > {
			return store.state.auth.userInfo
		})
		onMounted(async() = > {try {
				let res = await fetchUserInfo()
				if(res.code ! = =0) return new Error(res.msg)
				// Action is triggered by the store.dispatch method
				store.dispatch(`auth/${Types.SAVE_USER_INFO}`, res.data)
			} catch (error) {
				console.log(error)
			}
		})
		return {
			userInfo
		}
	}
})
Copy the code

Bring back to the top

✅ Vue – the router

This case mainly adopts the history mode, and the developer modifies the Mode Base as required

Go to :vue.config.js basic configuration

import { createRouter, createWebHistory } from 'vue-router'
import { constantRouterMap } from './router.config'

const router = createRouter({
	history: createWebHistory(process.env.BASE_URL),
	// When the back/forward button is pressed, it behaves like the browser's native behavior
	scrollBehavior(to, from, savedPosition) {
		if (savedPosition) {
			return savedPosition
		} else {
			return { top: 0}}},routes: constantRouterMap
})

export default router
Copy the code
import { RouteRecordRaw } from 'vue-router'

export const constantRouterMap: Array<RouteRecordRaw> = [
	{
		path: '/'.name: 'Home'.component: () = > import('@/views/layouts/index.vue'),
		redirect: '/home'.meta: {
			title: 'home'.keepAlive: false
		},
		children: [{path: '/home'.name: 'Home'.component: () = > import(/* webpackChunkName: "tabbar" */ '@/views/tabBar/home/index.vue'),
				meta: { title: 'home'.keepAlive: false.showTab: true}}, {path: '/demo'.name: 'Dome'.component: () = > import(/* webpackChunkName: "tabbar" */ '@/views/tabBar/dome/index.vue'),
				meta: { title: 'home'.keepAlive: false.showTab: true}}, {path: '/about'.name: 'About'.component: () = > import(/* webpackChunkName: "tabbar" */ '@/views/tabBar/about/index.vue'),
				meta: { title: 'About me'.keepAlive: false.showTab: true}}]}]Copy the code

More: Vue Router

Bring back to the top

✅ Webpack 4 Vue.config. js Basic configuration

If your Vue Router mode is hash

publicPath: '/'.Copy the code

If your Vue Router mode is History, the publicPath here is consistent with your Vue Router Base

publicPath: '/app/'.Copy the code
const IS_PROD = ['production'.'prod'].includes(process.env.NODE_ENV)

module.exports = {
	// publicPath: './', // The basic URL of the application package. Vue-router Hash mode is used
	publicPath: '/app/'.// The basic URL of the application package. Vue-router history is used
	outputDir: 'dist'.// Directory of the production environment build files
	assetsDir: 'static'.// outputDir static resources (js, CSS, img, fonts) directory
	lintOnSave: !IS_PROD,
	productionSourceMap: false.// If you don't need the source map for production, set it to false to speed up production builds.
	devServer: {
		port: 9020./ / the port number
		open: false.// Open the browser after startup
		overlay: {
			Display full screen overlay in the browser when compiler errors or warnings occur
			warnings: false.errors: true
		}
		// ...}}Copy the code

Bring back to the top

✅ Configure the alias

const path = require('path')
const resolve = dir= > path.join(__dirname, dir)
const IS_PROD = ['production'.'prod'].includes(process.env.NODE_ENV)

module.exports = {
	chainWebpack: config= > {
		// Add an alias
		config.resolve.alias
			.set(The '@', resolve('src'))
			.set('assets', resolve('src/assets'))
			.set('api', resolve('src/api'))
			.set('views', resolve('src/views'))
			.set('components', resolve('src/components'))}}Copy the code

Bring back to the top

✅ Configure proxy across domains

If your project requires cross-domain Settings, you need to annotate the vue.config.js proxy and configure the parameters accordingly

!!!!!!!!! Note: You will also need to addsrc/config/env.development.jsIn thebaseApiSet to ‘/’

module.exports = {
	devServer: {
		/ /...
		proxy: {
			// Configure cross-domain
			'/api': {
				target: 'https://test.xxx.com'.// Interface domain name
				// ws: true, // Whether webSockets are enabled
				changOrigin: true.// Enable the proxy to create a virtual server locally
				pathRewrite: {
					'^/api': '/'
				}
			}
		}
	}
}
Copy the code

Use for example: SRC/API /home.js

export function getUserInfo(params) {
	return request({
		url: '/api/userinfo'.method: 'post'.data: qs.stringify(params)
	})
}
Copy the code

Bring back to the top

✅ Configure package analysis

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
	chainWebpack: config= > {
		// Package analysis
		if (IS_PROD) {
			config.plugin('webpack-report').use(BundleAnalyzerPlugin, [
				{
					analyzerMode: 'static'}}}Copy the code
npm run build
Copy the code

Bring back to the top

✅ Configure externals to import CDN resources

In this version, CDN is no longer introduced. I tested using CDN and not using CDN. Not using CDN takes less time than using CDN. Many articles on the web test CDN speed block, this developer can be tested in practice.

In addition, the public CDN used in the project is unstable, and it takes time to resolve the domain name (please try to use the same domain name if you want to use it).

Because the page stops parsing every time it encounters a

The research has not been put on its own CDN server.

const defaultSettings = require('./src/config/index.js')
const name = defaultSettings.title || 'vue mobile template'
const IS_PROD = ['production'.'prod'].includes(process.env.NODE_ENV)

// externals
const externals = {
	vue: 'Vue'.'vue-router': 'VueRouter'.vuex: 'Vuex'.vant: 'vant'.axios: 'axios'
}
// CDN external chain will be inserted into index.html
const cdn = {
	// Development environment
	dev: {
		css: [].js: []},// Production environment
	build: {
		css: ['https://cdn.jsdelivr.net/npm/[email protected]/lib/index.css'].js: [
			'https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js'.'https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.min.js'.'https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js'.'https://cdn.jsdelivr.net/npm/[email protected]/dist/vuex.min.js'.'https://cdn.jsdelivr.net/npm/[email protected]/lib/index.min.js']}}module.exports = {
	configureWebpack: config= > {
		config.name = name
		// Modify the configuration for the production environment...
		if (IS_PROD) {
			// externals
			config.externals = externals
		}
	},
	chainWebpack: config= > {
		/** * Add CDN parameters to htmlWebpackPlugin configuration */
		config.plugin('html').tap(args= > {
			if (IS_PROD) {
				args[0].cdn = cdn.build
			} else {
				args[0].cdn = cdn.dev
			}
			return args
		})
	}
}
Copy the code

Add to public/index.html

<! -- CSS files using CDN --> <%for (var i in
      htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.css) { %>
      <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="preload" as="style" />
      <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet" />The < %} % > <! -- Use CDN accelerated JS file, configured under vue.config.js --> <%for (var i in
      htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
      <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>The < %} % >Copy the code

Bring back to the top

✅ remove the console log

Console. log for the test environment and local environment are retained

npm i -D babel-plugin-transform-remove-console
Copy the code

Configure in babel.config.js

VUE_APP_ENV is not NODE_ENV. The test environment remains console
const IS_PROD = ['production'.'prod'].includes(process.env.VUE_APP_ENV)
const plugins = [
	[
		'import',
		{
			libraryName: 'vant'.libraryDirectory: 'es'.style: true
		},
		'vant']]/ / remove the console log
if (IS_PROD) {
	plugins.push('transform-remove-console')}module.exports = {
	presets: [['@vue/cli-plugin-babel/preset', { useBuiltIns: 'entry' }]],
	plugins
}
Copy the code

Bring back to the top

✅ splitChunks package third-party modules separately

module.exports = {
	chainWebpack: config= > {
		config.when(IS_PROD, config= > {
			config
				.plugin('ScriptExtHtmlWebpackPlugin')
				.after('html')
				.use('script-ext-html-webpack-plugin'[{// The runtime is introduced as inline and does not stand alone
						inline: /runtime\.. *\.js$/
					}
				])
				.end()
			config.optimization.splitChunks({
				chunks: 'all'.cacheGroups: {
					// Multiple groups can be configured under cacheGroups. Each group is conditional on test and the module that matches the test condition
					commons: {
						name: 'chunk-commons'.test: resolve('src/components'),
						minChunks: 3.// Have been used at least three times to pack and separate
						priority: 5./ / priority
						reuseExistingChunk: true // Indicates whether to use the existing chunk. True indicates that if the chunk contains modules that have been extracted, no new chunk will be generated.
					},
					node_vendors: {
						name: 'chunk-libs'.chunks: 'initial'.// Package only the third party that you originally relied on
						test: /[\\/]node_modules[\\/]/,
						priority: 10
					},
					vantUI: {
						name: 'chunk-vantUI'.// Unpack vantUI separately
						priority: 20.// The number is heavily weighted to the one with the highest weight when multiple cacheGroups are met
						test: /[\\/]node_modules[\\/]_? vant(.*)/
					}
				}
			})
			config.optimization.runtimeChunk('single')}}}Copy the code

Bring back to the top

✅ gzip compression

May be an error, low installation version Reference address www.cnblogs.com/wuzhiquan/p…

// * Pack gzip
const assetsGzip = config= > {
	config.plugin('compression-webpack-plugin').use(require('compression-webpack-plugin'), [{filename: '[path].gz[query]'.algorithm: 'gzip'.test: /\.js$|\.html$|\.json$|\.css/,
			threshold: 10240.// Only resources larger than this value will be processed
			minRatio: 0.8.// Only resources whose compression ratio is less than this value will be processed
			deleteOriginalAssets: true // Delete the original file})}Copy the code

Bring back to the top

✅ uglifyjs compression

Note that using this plug-in requires converting ES6 code into ES5 code, which is not used in this project

// * Code compression
const codeUglify = config= > {
	config.plugin('uglifyjs-webpack-plugin').use(require('uglifyjs-webpack-plugin'), [{uglifyOptions: {
				// The production environment automatically deletes the console
				compress: {
					drop_debugger: true.drop_console: false.pure_funcs: ['console.log']}},sourceMap: false.parallel: true})}Copy the code

Bring back to the top

✅ VConsole mobile debugging

Reference address: github.com/AlloyTeam/A… Refer to the address: www.cnblogs.com/liyinSakura…

<! -- MobileConsole --><template>
	<teleport to="#vconsole">
		<div class="vc-tigger" @click="toggleVc"></div>
	</teleport>
</template>
<script lang="ts">
import { defineComponent, onUnmounted, reactive } from 'vue'
import VConsole from 'vconsole'
import config from '@/config'
import { useDOMCreate } from '@/hooks/useDOMCreate'
interface IState {
	lastClickTime: number
	count: number
	limit: number
	vConsole: any
}
export default defineComponent({
	name: 'MobileConsole'.props: {},
	setup() {
		useDOMCreate('vconsole')
		const state = reactive<IState>({
			lastClickTime: 0.count: 0.limit: ['production'.'prod'].includes(config.env || ' ')?5 : 0.vConsole: null
		})
		const hasClass = (obj: HTMLElement | null, cls: string) = > {
			returnobj? .className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))}const addClass = (obj: HTMLElement | null, cls: string) = > {
			if(! hasClass(obj, cls)) obj? .classList.add(cls) }const removeClass = (obj: HTMLElement | null, cls: string) = > {
			if(hasClass(obj, cls)) { obj? .classList.remove(cls) } }const toggleClass = (obj: HTMLElement | null, cls: string) = > {
			if (hasClass(obj, cls)) {
				removeClass(obj, cls)
			} else {
				addClass(obj, cls)
			}
		}
		const toggleVc = () = > {
			const nowTime = new Date().getTime()
			if (nowTime - state.lastClickTime < 3000) {
				state.count++
			} else {
				state.count = 0
			}
			state.lastClickTime = nowTime
			if (state.count >= state.limit) {
				if(! state.vConsole) { state.vConsole =new VConsole()
				}
				let vconDom = document.getElementById('__vconsole')
				toggleClass(vconDom, 'vconsole_show')
				state.count = 0
			}
		}
		onUnmounted(() = > {
			state.vConsole = null
		})
		return {
			toggleVc
		}
	}
})
</script>
<style lang="scss" scoped>
.vc-tigger {
	position: fixed;
	top: 0;
	left: 0;
	width: 20px;
	height: 20px;
	background: red;
}
</style>

Copy the code
  • Set the trap door in the component and click a few times to show the VConsole
    • Set by limit in app.vue
    • Development test environment can be displayed with one click
    • Production Environment click 5 times

teleport

The official document: v3.cn.vuejs.org/guide/telep…

The DOM element is there where the popbox and other components are referenced, which helps us separate the code from the component code, so we can see the DOM element composition better. Okay

UseDOMCreate makes it easy to create DOM elements without having to create the DOM elements needed by Teleport in index. HTML

Bring back to the top

✅ Dynamically sets the title

export const useDocumentTitle = (title: string) = > {
	document.title = title
}
Copy the code

The router/index. Ts is used

router.beforeEach((to, from, next) = > {
	useDocumentTitle(to.meta.title)
	next()
})
Copy the code

Bring back to the top

✅ Local storage storage encapsulation

Examples are under dome/storage/index.vue

Reference:

import { storage } from '@/utils/storage'
Copy the code

Call:

storage.set('data', originalData.value)
storageData.value = storage.get('data')
Copy the code

Bring back to the top

✅ configuration Jssdk

TODO: To be updated

Installation:

yarn add weixin-js-sdk
Copy the code

The type declaration is written in model/ weixin-js-sdK.d. ts

Since Apple Browser only recognizes the route entered for the first time, you need to configure the URL used first

  • router.ts

The JSSDK configuration here is for demonstration only. Normal service logic needs to be written with the back-end

import { isWeChat } from '.. /utils/index'
import { fetchWeChatAuth } from '@/api/WxController'
import { getQueryParams, phoneModel } from '@/utils'
import store from '@/store'

// The route starts to enter
router.beforeEach((to, from, next) = > {
  / /! Solve the problem of unsuccessful share signature in ios wechat, cache the url entered for the first time.
  if (window.entryUrl === undefined) {
    window.entryUrl = location.href.split(The '#') [0]}const { code } = getQueryParams<IQueryParams>()
		// Wechat authorized login in wechat browser
		/ / &&! store.state.auth.userInfo.name
		if (isWeChat()) {
			if (code) {
				store.commit('auth/STE_ISAUTH'.true)
				store.commit('auth/STE_CODE', code)
			}
			if(! store.state.auth.isAuth) { location.href = fetchWeChatAuth() } } next() }) router.afterEach((to, from, next) = > {
  let url
  if (phoneModel() === 'ios') {
    url = window.entryUrl
  } else {
    url = window.location.href
  }
	/ / save the url
  store.commit('link/SET_INIT_LINK', url)
})
Copy the code

store/Link

import { Module } from 'vuex'
import { IGlobalState } from '@/store/index'
import { ILinkState } from '@/store/modules/Link/interface'

const state: ILinkState = {
  initLink: ' '
}

const login: Module<ILinkState, IGlobalState> = {
  namespaced: true,
  state,
  mutations: {['SET_INIT_LINK'](state, data) {
      console.log(data)
      state.initLink = data
    }
  },
  actions: {}}export default login
Copy the code

Since the window does not have an entryUrl variable, you need to declare it in a declaration file

typings.ts

declare interface Window {
  entryUrl: any
}
Copy the code

Create the hooks function

hooks/useWxJsSdk.ts

UseWxJsSdk needs to be called once for each page that uses JSSDK, and then the other wrapped functions are used

Call:

Copy the code

Bring back to the top

✅ Eslint + Pettier unified development specification

Refer to Typescript code reviews

Install esLint prettier vetur in VScode

Prettierrc: write your own Pettier rule or prettier.config.js in the. Prettierrc file

module.exports =  {
  "wrap_line_length": 120."wrap_attributes": "auto"."eslintIntegration":true."overrides": [{"files": ".prettierrc"."options": {
        "parser": "json"}}].// A line of up to 100 characters
	printWidth: 100.// Use 4 Spaces for indentation
	tabWidth: 2.// Instead of indentation, use Spaces
	useTabs: false.// A semicolon is required at the end of the line
	semi: true.// Use single quotes
	singleQuote: true.// The key of the object is quoted only when necessary
	quoteProps: 'as-needed'.JSX uses double quotes instead of single quotes
	jsxSingleQuote: false.// Do not need a comma at the end
	trailingComma: 'none'.// Spaces are required at the beginning and end of braces
	bracketSpacing: true.// JSX tag Angle brackets require line breaks
	jsxBracketSameLine: false.// The arrow function, which has only one argument, also needs the parentheses avoid
	arrowParens: 'always'.// The range in which each file is formatted is the entire content of the file
	rangeStart: 0.rangeEnd: Infinity.// There is no need to write @prettier at the beginning of the file
	requirePragma: false.// There is no need to automatically insert @prettier at the beginning of a file
	insertPragma: false.// Use the default line folding standard always
	proseWrap: 'preserve'.// Depending on the display style, HTML should be folded or not
	htmlWhitespaceSensitivity: 'css'.// Use lf auto for newline
	endOfLine: 'lf'
}
Copy the code

. Eslintrc. Js configuration

module.exports = {
  root: true.env: {
    browser: true.node: true.es6: true
  },
  extends: [
    'plugin:vue/vue3-essential'.'eslint:recommended'.'@vue/typescript/recommended'.'@vue/prettier'.'@vue/prettier/@typescript-eslint'].parserOptions: {
    ecmaVersion: 2020
  },
  rules: {
    // Do not use var
    'no-var': 'error'.'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off'.'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'.'@typescript-eslint/no-empty-function': 0.'@typescript-eslint/no-var-requires': 0.'@typescript-eslint/interface-name-prefix': 0.'@typescript-eslint/no-explicit-any': 0 // TODO}};Copy the code

Vscode setting. Json Settings

{
  "[vue]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[tavascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  // Format with eslint when saving
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  // The two will conflict when formatting JS, so you need to disable the default JS formatter
  "javascript.format.enable": false."typescript.format.enable": false."vetur.format.defaultFormatter.html": "none".// js/ts uses eslint to prevent prettier in vetur from formatting in conflict with ESLint
  "vetur.format.defaultFormatter.js": "none"."vetur.format.defaultFormatter.ts": "none"."files.eol": "\n"."editor.tabSize": 2."editor.formatOnSave": true.// "editor.defaultFormatter": "esbenp.prettier-vscode",
  "eslint.autoFixOnSave": true."eslint.validate": [
    "javascript"."javascriptreact",
    {
      "language": "typescript"."autoFix": true}]."typescript.tsdk": "node_modules/typescript/lib"
}
Copy the code

Bring back to the top

thanks

vue-h5-template vue-cli4-config vue-element-admin