Project address vuE-CLI3-project welcome star
The original address www.ccode.live/lentoo/list…
1. Create a VUE project
Most of you already know how to create projects, so you can skip this section and move on to the next.
1.1 installation @ vue/cli
# Global install VUE-CLI scaffolding
npm install -g @vue/cli
Copy the code
Wait until the installation is complete to start the next step
1.2 Initializing the Project
vue create vue-cli3-project
Copy the code
- Select a preset
babel
eslint
We chose Manually select features
Press Enter to select the plug-in
- Plug-in option
Babel, Router, Vuex, Css preprocessor, Linter/Formatter check, Unit test framework
- Routing Mode selection
Whether to use routing in History mode (Yes)
- Select a CSS preprocessor (Sass/SCSS)
- Select an ESLint configuration
ESLint + Standard Config. I prefer this code specification
- When will the selection take place
eslint
check
The option (Lint on save) to save is to check
If you are using the vscode editor, you can configure the eslint plugin for automatic code formatting
- Do you want to save the default configuration? (y)
If yes, the next time you create a VUE project, you can use this default file directly without having to configure it.
Wait for dependencies to complete
2. Automatically register global components
Create a global directory under the Components directory to place components that need to be registered globally.
Index.js simply imports main.vue and exports the component object
components
index.js
// components/index.js
import Vue from 'vue'
// Automatically loads.js files in the global directory
const componentsContext = require.context('./global'.true, /\.js$/)
componentsContext.keys().forEach(component= > {
const componentConfig = componentsContext(component)
/** * compatible with import export and require module.export */
const ctrl = componentConfig.default || componentConfig
Vue.component(ctrl.name, ctrl)
})
Copy the code
Finally, import the index.js file from main.js
3. Routes are imported automatically
To add a new page, you need to configure the information for that page in the Routing configuration.
How do we make routing cleaner if we have more and more pages?
3.1 Splitting routes
Split routes based on service modules
Export an array of routing configurations in each submodule
Import all submodules in the root index.js
3.2 Automatically Scan for submodule routes and import them
As our business grew bigger and bigger, every time we added a new business module, we would add a new sub-routing module under the route and import it in index.js.
So how do you simplify this operation?
With the auto-scan global component registration above, we can also automatically scan submodule routes and import them
4. Use Node to generate components
As a front-end developer, wouldn’t it be a waste to have something so good in Node if it didn’t work?
.vue
template
script
style
index.js
So can we use Node to help us do this? Just tell Node the name of the component it generated for me. Let Node do the rest
4.1 Generate components from Node
- Install the
chalk
This plugin can make our console output statements color-coded
npm install chalk --save-dev
Copy the code
Create a scripts folder in the root directory,
Add a new generatecomponent.js file to place the code that generated the component,
Add a template.js file that holds the code for the component template
- template.js
// template.js
module.exports = {
vueTemplate: compoenntName= > {
return `<template>
<div class="${compoenntName}">
${compoenntName}Component </div> </template> <script> export default {name: '${compoenntName}'
}
</script>
<style lang="scss" scoped>
.${compoenntName} {
}
</style>
`
},
entryTemplate: `import Main from './main.vue'
export default Main`
}
Copy the code
- 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}The file already exists)
return
}
return new Promise((resolve, reject) = > {
fs.writeFile(path, data, 'utf8', err => {
if (err) {
errorLog(err.message)
reject(err)
} else {
resolve(true)
}
})
})
}
log('Please enter the name of the component to build. If you want to build global components, prefix global/.')
let componentName = ' '
process.stdin.on('data'.async chunk => {
const inputName = String(chunk).trim().toString()
/** * Component directory path */
const componentDirectory = resolve('.. /src/components', inputName)
/** * Vue component path */
const componentVueName = resolve(componentDirectory, 'main.vue')
/** * entry file path */
const entryComponentName = resolve(componentDirectory, 'index.js')
const hasComponentDirectory = fs.existsSync(componentDirectory)
if (hasComponentDirectory) {
errorLog(`${inputName}The component directory already exists, please re-enter ')
return
} else {
log(The Component directory is being generated${componentDirectory}`)
await dotExistDirectoryCreate(componentDirectory)
// fs.mkdirSync(componentDirectory);
}
try {
if (inputName.includes('/')) {
const inputArr = inputName.split('/')
componentName = inputArr[inputArr.length - 1]}else {
componentName = inputName
}
log('Creating vUE files${componentVueName}`)
await generateFile(componentVueName, vueTemplate(componentName))
log('An Entry file is being generated${entryComponentName}`)
await generateFile(entryComponentName, entryTemplate)
successLog('Generated successfully')}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)})})}// Create a directory recursively
function mkdirs (directory, callback) {
var exists = fs.existsSync(directory)
if (exists) {
callback()
} else {
mkdirs(path.dirname(directory), function () {
fs.mkdirSync(directory)
callback()
})
}
}
Copy the code
- Configuration package. Json
"new:comp": "node ./scripts/generateComponent"
Copy the code
- perform
NPM run new:comp
If yarn is used, it is yarn new:comp
4.2 Generate page components from Node
Now that we can generate components from Node, we can also generate page components from the same logic. You only need to modify the logic that generates the component code slightly. Create a new generateView.js file in the scripts directory
// 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}The file already exists)
return
}
return new Promise((resolve, reject) = > {
fs.writeFile(path, data, 'utf8', err => {
if (err) {
errorLog(err.message)
reject(err)
} else {
resolve(true)
}
})
})
}
log('Please enter the name of the page component to be generated, it will be generated in the views/ directory')
let componentName = ' '
process.stdin.on('data'.async chunk => {
const inputName = String(chunk).trim().toString()
/** * Vue page component path */
let componentVueName = resolve('.. /src/views', inputName)
// If it does not end with.vue, it is automatically added
if(! componentVueName.endsWith('.vue')) {
componentVueName += '.vue'
}
/** * Vue component directory path */
const componentDirectory = path.dirname(componentVueName)
const hasComponentExists = fs.existsSync(componentVueName)
if (hasComponentExists) {
errorLog(`${inputName}The page component already exists, please re-enter ')
return
} else {
log(The Component directory is being generated${componentDirectory}`)
await dotExistDirectoryCreate(componentDirectory)
}
try {
if (inputName.includes('/')) {
const inputArr = inputName.split('/')
componentName = inputArr[inputArr.length - 1]}else {
componentName = inputName
}
log('Creating vUE files${componentVueName}`)
await generateFile(componentVueName, vueTemplate(componentName))
successLog('Generated successfully')}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)})})}// Create a directory recursively
function mkdirs (directory, callback) {
var exists = fs.existsSync(directory)
if (exists) {
callback()
} else {
mkdirs(path.dirname(directory), function () {
fs.mkdirSync(directory)
callback()
})
}
}
Copy the code
- Configure package.json to add one
scripts
The script
"new:view": "node ./scripts/generateView"
Copy the code
- perform
NPM run new: View if NPM is used
If yarn is used, it is yarn new:view
5. Axios encapsulation
- Install axios
npm install axios --save
// or
yarn add axios
Copy the code
5.1 Configuring Different Environments
Create three new environment variable files in the root directory
dev
dev
test
test
# // .env
NODE_ENV = "development"
BASE_URL = "https://easy-mock.com/mock/5c4c50b9888ef15de01bec2c/api"
Copy the code
Then create a new vue.config.js in the root directory
// vue.config.js module.exports = { chainWebpack: Config => {// Here is the environment configuration, different environment corresponding to different BASE_URL, so that the AXIOS request address is different config.plugin('define').tap(args => {
args[0]['process.env'].BASE_URL = JSON.stringify(process.env.BASE_URL)
return args
})
}
}
Copy the code
Then create an API folder in the SRC directory and create an index.js to configure axios configuration information
// src/api/index.js import axios from 'axios' import router from '.. /router' import {Message} from 'elemental-ui' const service = axios.create({// set timeout: 60000, baseURL: Process.env.base_url}) // For post requests, we need a request header, so we can do a default setting here // that is, set the post request header to application/x-www-form-urlencoded; charset=UTF-8 service.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'' export default serviceCopy the code
5.2 Request response Encapsulation
import axios from 'axios' import router from '.. /router' import {Message} from 'elemental-ui' const service = axios.create({// set timeout: 60000, baseURL: Process. The env. BASE_URL})/intercept * before * * * request for processing needs to * before the request operation/service. The interceptors. Request. Use (config = > {const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }, (error) => {return promise.reject (error)}) /** * Request response interception * is used to process operations that need to be performed after the request is returned */ Service. The interceptors. Response. Use (response = > {const responseCode = response. The status / / if the returned a status code of 200, shows that interface request is successful, If (responseCode === 200) {return promise. resolve(response)} else {return Promise.reject(response)}}, error => {// the server returns something that does not start with 2, Const responseCode = error.response.status switch (responseCode) {// 401: not logged in case 401: / / jump to login page of the router. The replace ({path: '/ login, query: {redirect: router. CurrentRoute. FullPath}}) break / / 403: Token expired case 403: // Error Message Message({type: 'error', Message: }) // Clear token localstorage. removeItem('token') // redirect to the login page and pass the fullPath of the page to be accessed, SetTimeout (() => {router.replace({path: '/login', query: {redirect: The router. CurrentRoute. FullPath}})}, 1000) break / / 404 request there is no case 404: Message ({Message: 'there is no network request, type: 'error'}) break / / other errors, directly thrown error default: Message ({Message: error. The response. The data. The Message, type: 'error' }) } return Promise.reject(error) }) export default serviceCopy the code
The Message method is a Message prompt component provided by Element-UI that you can replace with your own Message prompt component
5.3 Handling Network Disconnection
Add processing logic to response interception
service.interceptors.response.use(response= > {
const responseCode = response.status
// If the returned status code is 200, the interface request succeeds and data can be obtained normally
// Otherwise, an error is thrown
if (responseCode === 200) {
return Promise.resolve(response.data)
} else {
return Promise.reject(response)
}
}, error => {
// The network is disconnected or the request times out
if(! error.response) {// Request timeout status
if (error.message.includes('timeout')) {
console.log('Out of time')
Message.error('Request timed out. Please check network connection.')}else {
// You can display the disconnected component
console.log('Disconnected')
Message.error('Request failed, please check network connection')}return
}
// Omit other code
return Promise.reject(error)
})
Copy the code
5.4 Uploading Encapsulated Images
// 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
}
Copy the code
call
async uploadFile (e) {
const file = document.getElementById('file').files[0]
const formdata = new FormData()
formdata.append('file', file)
await uploadFile(formdata)
}
Copy the code
5.5 Requesting to Display the Loading Effect
let loading = null
service.interceptors.request.use(config= > {
// Display the load box before the request
loading = Loading.service({
text: 'Loading...... '
})
// Omit other code
return config
}, (error) => {
return Promise.reject(error)
})
service.interceptors.response.use(response= > {
// Close the load box after requesting a response
if (loading) {
loading.close()
}
// Omit other code
}, error => {
// Close the load box after requesting a response
if (loading) {
loading.close()
}
// Omit other code
return Promise.reject(error)
})
Copy the code
6. Use opportunely Mixins
6.1 Encapsulating store Common methods
Consider a scenario where we encapsulate a function to get a news list with Vuex
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]
}
}
})
Copy the code
And then on the news list page, we call Action and getters with mapAction and mapGetters and we need to write this code
import { mapActions, mapGetters } from 'vuex'computed: { ... mapGetters(['getNewsResponse']) }, methods: { ... mapActions(['NEWS_LIST'])}Copy the code
Suppose, on another page, we need to call the interface to get the news list again, so we have to write the above code again, right?
Copy and paste is dry there?
If an interface suddenly takes an argument, wouldn’t every code that uses the interface have to take that argument?
Copy and paste for a while cool, demand a change you cool
Since it is repetitive code, we must reuse it, where the mixins provided by Vue come in handy
- Packaging news – a mixin. Js in
src
So let’s create onemixins
Directory to manage all mixinsnews-mixin.js
import { mapActions, mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['getNewsResponse'])},methods: {
...mapActions(['NEWS_LIST'])}}Copy the code
You can then call this method directly by introducing the mixin in the component you want to use. No matter how many pages, once you introduce this mixin, you can use it directly.
If the requirements change, you only need to modify the mixin file
// 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)
}
}
Copy the code
6.2 extensions
In addition to the common way to encapsulate VUex, there are many other things that can encapsulate vuex. Examples include paging objects, tabular data, common methods, and so on. Can see a lot
If you use it frequently in multiple places, consider packaging it as a mixin, but make sure to comment it out. Otherwise, someone will scold you behind your back!! ~ ~ you understand
7. To optimize
7.1 gzip compression
- The installation
compression-webpack-plugin
The plug-in
npm install compression-webpack-plugin --save-dev
// or
yarn add compression-webpack-plugin --dev
Copy the code
- Add the configuration in vue.config.js
// vue.config.js
const CompressionPlugin = require('compression-webpack-plugin'Module.exports = {chainWebpack: config => {// config.plugin(module. Exports = {chainWebpack: config => {// 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 Enables GZip compression
config
.plugin('compression')
.use(CompressionPlugin, {
asset: '[path].gz[query]',
algorithm: 'gzip'.test: new RegExp('\ \. (' + ['js'.'css'].join('|') + '$'),
threshold: 10240,
minRatio: 0.8,
cache: true
})
.tap(args => { })
// #endregion}}}Copy the code
NPM run build can see generated.gz file OK. If your server uses Nginx, nginx also needs to be configured to enable GZIP
7.2 Third-party Libraries Reference CDN
For vue, VUE-Router, Vuex, AXIos, element-UI and other libraries that are not frequently changed, we let WebPack not pack them. Introducing them through CDN can reduce the size of the code, reduce the bandwidth of the server, and cache these files to the client. The client will load faster.
- configuration
vue.config.js
const CompressionPlugin = require('compression-webpack-plugin'Module. Exports = {chainWebpack: config => {// omit other code ···· //#region Ignores files packed by the build environment
var externals = {
vue: 'Vue',
axios: 'axios'.'element-ui': 'ELEMENT'.'vue-router': 'VueRouter',
vuex: 'Vuex'
}
config.externals(externals)
const cdn = {
css: [
// element-ui css
'//unpkg.com/element-ui/lib/theme-chalk/index.css'
],
js: [
// vue
'/ / cdn.staticfile.org/vue/2.5.22/vue.min.js',
// vue-router
'/ / cdn.staticfile.org/vue-router/3.0.2/vue-router.min.js',
// vuex
'/ / cdn.staticfile.org/vuex/3.1.0/vuex.min.js',
// axios
'/ / cdn.staticfile.org/axios/0.19.0-beta.1/axios.min.js',
// element-ui js
'//unpkg.com/element-ui/lib/index.js'
]
}
config.plugin('html')
.tap(args => {
args[0].cdn = cdn
return args
})
// #endregion}}}Copy the code
- Modify the
index.html
<! --public/index.html-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<% if (process.env.NODE_ENV= = ='production') { %>
<% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
<link href="<%=css%>" rel="preload" as="style">
<link rel="stylesheet" href="<%=css%>" as="style">
<%} % >
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
<link href="<%=js%>" rel="preload" as="script">
<script src="<%=js%>"></script>
<%} % >
<%} % >
<title>vue-cli3-project</title>
</head>
<body>
<noscript>
<strong>We're sorry but vue-cli3-project doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<! -- built files will be auto injected -->
</body>
</html>
Copy the code
7.3 total CDN
We have replaced the third-party library with CDN, so can we also use CDN for the JS and CSS files generated after our build?
Apply for your own CDN domain name
If you want to upload your own resources to CDN, you must have your own CDN domain name. If not, you can register and apply for one on qiniuyun official website
- Register a qiuniuyun account
- Create storage space in the qiuniuyun object storage module
- Enter storage space information
- Make sure to create
- After the storage space is created, the console page for the storage space is displayed
- One of these domains is your test domain
- We can upload ours to content Management
js
,css
And so on file, but our file so much, a upload is obviously unreasonable. Not if I ask you to.
At this point, these batch and repeat operations should be handled by our Node, let’s use Node to batch upload our resource files
Upload the generated JS and CSS resources to qiniu CDN
There is an introduction on how to upload files through Node in the documentation center of qiuniuyun official website. Those who are interested can go to study it themselves.
- To view
AccessKey
andSecretKey
In your personal panel -> Key Management, these two keys will be used later
- Install the required plug-ins
npm install qiniu glob mime --save-dev
Copy the code
- in
scripts
Create one in the directoryupcdn.js
file
// /scripts/upcdn.js
const qiniu = require('qiniu')
const glob = require('glob')
const mime = require('mime')
const path = require('path')
const isWindow = /^win/.test(process.platform)
let pre = path.resolve(__dirname, '.. /dist/') + (isWindow ? '\ \' : ' ')
const files = glob.sync(
`${path.join(
__dirname,
'.. /dist/**/*.? (js|css|map|png|jpg|svg|woff|woff2|ttf|eot)'
)}`
)
pre = pre.replace(/\\/g.'/')
const options = {
scope: 'source' // Space object name
}
var config = {
qiniu: {
accessKey: ' '.// AccessKey in personal key management
secretKey: ' '.// SecretKey in personal central key management
bucket: options.scope,
domain: 'http://ply4cszel.bkt.clouddn.com'}}var accessKey = config.qiniu.accessKey
var secretKey = config.qiniu.secretKey
var mac = new qiniu.auth.digest.Mac(accessKey, secretKey)
var putPolicy = new qiniu.rs.PutPolicy(options)
var uploadToken = putPolicy.uploadToken(mac)
var cf = new qiniu.conf.Config({
zone: qiniu.zone.Zone_z2
})
var formUploader = new qiniu.form_up.FormUploader(cf)
async function uploadFileCDN (files) {
files.map(async file => {
const key = getFileKey(pre, file)
try {
await uploadFIle(key, file)
console.log('Upload successful key:${key}`)}catch (err) {
console.log('error', err)
}
})
}
async function uploadFIle (key, localFile) {
const extname = path.extname(localFile)
const mimeName = mime.getType(extname)
const putExtra = new qiniu.form_up.PutExtra({ mimeType: mimeName })
return new Promise((resolve, reject) = > {
formUploader.putFile(uploadToken, key, localFile, putExtra, function (respErr, respBody, respInfo) {
if (respErr) {
reject(respErr)
}
resolve({ respBody, respInfo })
})
})
}
function getFileKey (pre, file) {
if (file.indexOf(pre) > - 1) {
const key = file.split(pre)[1]
return key.startsWith('/')? key.substring(1) : key
}
return file
}
(async() = > {console.time('Upload file to CDN')
await uploadFileCDN(files)
console.timeEnd('Upload file to CDN')
})()
Copy the code
Modify publicPath
Modify the configuration information of vue.config.js to make its publicPath point to the domain name of our CDN
const IS_PROD = process.env.NODE_ENV === 'production'
const cdnDomian = 'http://ply4cszel.bkt.clouddn.com'
module.exports = {
publicPath: IS_PROD ? cdnDomian : '/', // omit other code...Copy the code
Modify package.json configuration
Modify package.json configuration to automatically upload resource files to CDN server after we finish build
"build": "vue-cli-service build --mode prod && node ./scripts/upcdn.js".Copy the code
Run to view the effect
npm run build
cdn
8. Docker deployment
This is a centOS7 environment, but using a different system, you can refer to the installation method of other systems
8.1 installation docker
- Update software library
yum update -y
Copy the code
- Install the docker
yum install docker
Copy the code
- Start the Docker service
service docker start
Copy the code
- Install the docker – compose
Yum install docker-compose yum install docker-composeCopy the code
8.2 write a docker – compose. Yaml
version: '2.1'
services:
nginx:
restart: always
image: nginx
volumes:
# ~ / var/local/nginx/nginx. Conf for native directory, / etc/nginx directory for container
- /var/local/nginx/nginx.conf:/etc/nginx/nginx.conf
Dist = /usr/ SRC /app = /usr/ SRC /app
- /var/local/app/dist:/usr/src/app
ports:
- 80:80
privileged: true
Copy the code
8.3 Writing the nginx.conf configuration
#user nobody;
worker_processes 2;
# Working mode and connection number online
events {
worker_connections 1024; # Maximum number of concurrent processes processed by a single worker process
}
http {
include mime.types;
default_type application/octet-stream;
# sendFile specifies whether nginx calls sendfile (zero copy) to output files. For common applications,
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
# open GZIP
gzip on;
Listen on port 80 and forward the request to port 3000
server {
# monitor port
listen 80;
# Encoding format
charset utf-8;
Front-end static file resources
location / {
root /usr/src/app;
index index.html index.htm;
try_files $uri $uri/ @rewrites;
}
If the resource is not matched, point the URL to index. HTML and use it in vue-router history modelocation @rewrites { rewrite ^(.*)$ /index.html last; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; }}}Copy the code
8.4 perform docker – compose
docker-compose -d up
Copy the code
8.5 Docker + Jenkins automated deployment
Docker + Jenkins can be used to implement the code submitted to Github after automatic deployment environment, this to talk about too much content, interested can see my article
Build docker+ Jenkins + Node.js automatic deployment environment from zero
6. Extension
- Deploy node projects automatically using PM2
- Build an SSR application with VUE-CLI3
If you have any better way to practice, welcome to comment section!!
Project address vuE-CLI3-project welcome star
The original address www.ccode.live/lentoo/list…
Welcome to attention
Welcome to pay attention to the public account “code development”, share the latest technical information every day