Blame view

README.md 18 KB
406803045 committed
1 2
# vue-h5-template

sunnie committed
3
基于 vue-cli3.0+webpack 4+vant ui + sass+ rem 适配方案+axios 封装,构建手机端模板脚手架
宋楠 committed
4

宋楠 committed
5
[关于项目介绍](https://juejin.im/post/5cfefc73f265da1bba58f9f7)
406803045 committed
6

sunnie committed
7 8
[demo](https://solui.cn/vue-h5-template/#/)建议手机端查看

宋楠 committed
9
### Node 版本要求
sunnie committed
10

sunnie committed
11 12
`Vue CLI` 需要 Node.js 8.9 或更高版本 (推荐 8.11.0+)。你可以使用 [nvm](https://github.com/nvm-sh/nvm)
[nvm-windows](https://github.com/coreybutler/nvm-windows) 在同一台电脑中管理多个 Node 版本。
宋楠 committed
13 14

本示例 Node.js 12.14.1
sunnie committed
15 16 17

<span id="top">目录</span>

sunnie committed
18
- [√ Vue-cli4](https://cli.vuejs.org/zh/guide/)
sunnie committed
19 20
- [√ 配置多环境变量](#env)
- [√ rem 适配方案](#rem)
sunnie committed
21 22 23 24
- [√ VantUI 组件按需加载](#vant)
- [√ Sass](#sass)
- [√ Webpack 4](#webpack)
- [√ Vuex](#vuex)
sunnie committed
25
- [√ Axios 封装及接口管理](#axios)
sunnie committed
26 27
- [√ Vue-router](#router)
- [√ vue.config.js 基础配置](#base)
宋楠 committed
28 29 30 31 32 33 34 35
- [√ 配置 proxy 跨域](#proxy)
- [√ 配置 alias 别名](#alias)
- [√ 配置 打包分析](#bundle)
- [√ 配置 externals 引入 cdn 资源 ](#externals)
- [√ 去掉 console.log ](#console)
- [√ splitChunks ](#console)
- [√ 添加 IE 兼容 ](#ie)

sunnie committed
36 37 38 39 40 41

* Vuex
* Axios 封装
* 生产环境 cdn 优化首屏加速
* babel 低版本浏览器兼容
* Eslint+Pettier 统一开发规范
sunnie committed
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61

### <span id="env">✅ 配置多环境变量 </span>

`package.json` 里的 `scripts` 配置 `serve` `stage` `build`,通过 `--mode xxx` 来执行不同环境

- 通过 `npm run serve` 启动本地 , 执行 `development`
- 通过 `npm run stage` 打包测试 , 执行 `staging`
- 通过 `npm run build` 打包正式 , 执行 `production`

```javascript
"scripts": {
  "serve": "vue-cli-service serve --open",
  "stage": "vue-cli-service build --mode staging",
  "build": "vue-cli-service build",
}
```

##### 配置介绍

&emsp;&emsp;`VUE_APP_` 开头的变量,在代码中可以通过 `process.env.VUE_APP_` 访问。  
sunnie committed
62 63
&emsp;&emsp;比如,`VUE_APP_ENV = 'development'` 通过`process.env.VUE_APP_ENV` 访问 &emsp;&emsp; 除了 `VUE_APP_*` 变量之外
,在你的应用代码中始终可用的还有两个特殊的变量`NODE_ENV``BASE_URL`
sunnie committed
64 65 66 67

在项目根目录中新建.env

- .env.development 本地开发环境配置
sunnie committed
68

sunnie committed
69 70 71 72
```bash
NODE_ENV='development'
# must start with VUE_APP_
VUE_APP_ENV = 'development'
sunnie committed
73

sunnie committed
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
```

- .env.staging 测试环境配置

```bash
NODE_ENV='production'
# must start with VUE_APP_
VUE_APP_ENV = 'staging'
```

- .env.production 正式环境配置

```bash
 NODE_ENV='production'
# must start with VUE_APP_
VUE_APP_ENV = 'production'
```

这里我们并没有定义很多变量,只定义了基础的 VUE_APP_ENV `development` `staging` `production`  
sunnie committed
93 94 95 96
变量我们统一在 `src/config/env.*.js` 里进行管理。

这里有个问题,既然这里有了根据不同环境设置变量的文件,为什么还要去 config 下新建三个对应的文件呢? **修改起来方便,不需
要重启项目,符合开发习惯。**
sunnie committed
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125

config/index.js

```javascript
// 根据环境引入不同配置 process.env.NODE_ENV
const config = require('./env.' + process.env.VUE_APP_ENV)
module.exports = config
```

配置对应环境的变量,拿本地环境文件 `env.development.js` 举例,用户可以根据需求修改

```javascript
// 本地环境配置
module.exports = {
  title: 'vue-h5-template',
  baseUrl: 'http://localhost:9018', // 项目地址
  baseApi: 'https://test.xxx.com/api', // 本地api请求地址
  APPID: 'xxx',
  APPSECRET: 'xxx'
}
```

根据环境不同,变量就会不同了

```javascript
// 根据环境不同引入不同baseApi地址
import {baseApi} from '@/config'
console.log(baseApi)
```
sunnie committed
126

sunnie committed
127
[▲ 回顶部](#top)
sunnie committed
128

sunnie committed
129
### <span id="rem">✅ rem 适配方案 </span>
sunnie committed
130

sunnie committed
131
不用担心,项目已经配置好了 `rem` 适配, 下面仅做介绍:
sunnie committed
132

sunnie committed
133
Vant 中的样式默认使用`px`作为单位,如果需要使用`rem`单位,推荐使用以下两个工具:
sunnie committed
134

sunnie committed
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
- [postcss-pxtorem](https://github.com/cuth/postcss-pxtorem) 是一款 `postcss` 插件,用于将单位转化为 `rem`
- [lib-flexible](https://github.com/amfe/lib-flexible) 用于设置 `rem` 基准值

##### PostCSS 配置

下面提供了一份基本的 `postcss` 配置,可以在此配置的基础上根据项目需求进行修改

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

更多详细信息: [vant](https://youzan.github.io/vant/#/zh-CN/quickstart#jin-jie-yong-fa)

**新手必看,老鸟跳过**

很多小伙伴会问我,适配的问题。

sunnie committed
163 164
我们知道 `1rem` 等于`html` 根元素设定的 `font-size``px` 值。Vant UI 设置 `rootValue: 37.5`,你可以看到在 iPhone 6 下
看到 (`1rem 等于 37.5px`):
sunnie committed
165 166 167 168 169

```html
<html data-dpr="1" style="font-size: 37.5px;"></html>
```

sunnie committed
170
切换不同的机型,根元素可能会有不同的`font-size`。当你写 css px 样式时,会被程序换算成 `rem` 达到适配。
sunnie committed
171

sunnie committed
172
因为我们用了 Vant 的组件,需要按照 `rootValue: 37.5` 来写样式。
sunnie committed
173

sunnie committed
174
举个例子:设计给了你一张 750px \* 1334px 图片,在 iPhone6 上铺满屏幕,其他机型适配。
sunnie committed
175

sunnie committed
176 177 178
-`rootValue: 70` , 样式 `width: 750px;height: 1334px;` 图片会撑满 iPhone6 屏幕,这个时候切换其他机型,图片也会跟着撑
  满。
-`rootValue: 37.5` 的时候,样式 `width: 375px;height: 667px;` 图片会撑满 iPhone6 屏幕。
sunnie committed
179

sunnie committed
180
也就是 iphone 6 下 375px 宽度写 CSS。其他的你就可以根据你设计图,去写对应的样式就可以了。
sunnie committed
181

sunnie committed
182
当然,想要撑满屏幕你可以使用 100%,这里只是举例说明。
sunnie committed
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202

```html
<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>
```

[▲ 回顶部](#top)

宋楠 committed
203 204
### <span id="vant">✅ VantUI 组件按需加载 </span>

sunnie committed
205 206 207
项目采
[Vant 自动按需引入组件 (推荐)](https://youzan.github.io/vant/#/zh-CN/quickstart#fang-shi-yi.-zi-dong-an-xu-yin-ru-zu-jian-tui-jian)
面安装插件介绍:
宋楠 committed
208

sunnie committed
209 210
[babel-plugin-import](https://github.com/ant-design/babel-plugin-import) 是一款 `babel` 插件,它会在编译过程中将
`import` 的写法自动转换为按需引入的方式
宋楠 committed
211

sunnie committed
212
#### 安装插件
宋楠 committed
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233

```javascript
npm i babel-plugin-import -D

// 对于使用 babel7 的用户,可以在 babel.config.js 中配置
module.exports = {
  presets: [['@vue/cli-plugin-babel/preset', {useBuiltIns: 'entry'}]],
  plugins: [
    [
      'import',
      {
        libraryName: 'vant',
        libraryDirectory: 'es',
        style: true
      },
      'vant'
    ]
  ]
}
```

sunnie committed
234
#### 使用组件
宋楠 committed
235 236 237 238 239 240 241 242 243 244 245

项目在 `src/plugins/vant.js` 下统一管理组件,用哪个引入哪个,无需在页面里重复引用

```javascript
// 按需全局引入 vant组件
import Vue from 'vue'
import {Button, List, Cell, Tabbar, TabbarItem} from 'vant'
Vue.use(Button)
Vue.use(Cell)
Vue.use(List)
Vue.use(Tabbar).use(TabbarItem)
sunnie committed
246 247 248 249 250 251 252
```

[▲ 回顶部](#top)

### <span id="sass">✅ Sass </span>

首先 你可能会遇到 `node-sass` 安装不成功,别放弃多试几次!!!
宋楠 committed
253

sunnie committed
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
`src/assets/css/`文件夹下包含了三个文件

- `index.scss` 主入口,主要引入其他两个 scss 文件,和公共样式
- `variables.scss` 定义变量
- `mixin.scss` 定义 `mixin` 方法

你可以直接在 vue 文件下写 sass 语法

```html
<style lang="scss" scoped>
  .demo {
    .title {
      font-size: 12px;
    }
  }
</style>
宋楠 committed
270
```
sunnie committed
271

宋楠 committed
272 273 274 275
[▲ 回顶部](#top)

### <span id="sass">✅ Sass </span>

sunnie committed
276 277 278
首先 你可能会遇到 `node-sass` 安装不成功,别放弃多试几次!!!

`src/assets/css/`文件夹下包含了三个文件
宋楠 committed
279

sunnie committed
280
- `index.scss` 主入口,主要引入其他两个 scss 文件,和公共样式
宋楠 committed
281 282 283
- `variables.scss` 定义变量
- `mixin.scss` 定义 `mixin` 方法

sunnie committed
284
你可以直接在 vue 文件下写 sass 语法
宋楠 committed
285 286 287

```html
<style lang="scss" scoped>
sunnie committed
288 289 290 291 292
  .demo {
    .title {
      font-size: 12px;
    }
  }
宋楠 committed
293
</style>
sunnie committed
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
```

[▲ 回顶部](#top)

### <span id="base">✅ Vue-router </span>

本案例采用 `hash` 模式,开发者根据需求修改 `mode` `base`

**注意**:如果你使用了 `history` 模式,`vue.config.js` 中的 `publicPath` 要做对应的**修改**

前往:[vue.config.js 基础配置](#base)

```javascript
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)
export const router = [
  {
    path: '/',
    name: 'index',
    component: () => import('@/views/home/index'), // 路由懒加载
    meta: {
      title: '首页', // 页面标题
      keepAlive: false // keep-alive 标识
    }
  }
]
const createRouter = () =>
  new Router({
    // mode: 'history', // 如果你是 history模式 需要配置 vue.config.js publicPath
    // base: '/app/',
    scrollBehavior: () => ({y: 0}),
    routes: router
  })

export default createRouter()
```

更多:[Vue Router](https://router.vuejs.org/zh/)
宋楠 committed
334

sunnie committed
335 336
[▲ 回顶部](#top)

sunnie committed
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
### <span id="axios">✅ Axios 封装及接口管理</span>

`utils/request.js` 封装 axios ,开发者需要根据后台接口做修改。

- `service.interceptors.request.use` 里可以设置请求头,比如设置 `token`
- `config.hideloading` 是在 api 文件夹下的接口参数里设置,下文会讲
- `service.interceptors.response.use` 里可以对接口返回数据处理,比如 401 删除本地信息,重新登录

```javascript
import axios from 'axios'
import store from '@/store'
import {Toast} from 'vant'
// 根据环境不同引入不同api地址
import {baseApi} from '@/config'
// create an axios instance
const service = axios.create({
  baseURL: baseApi, // url = base api url + request url
  withCredentials: true, // send cookies when cross-domain requests
  timeout: 5000 // request timeout
})

// request 拦截器 request interceptor
service.interceptors.request.use(
  config => {
    // 不传递默认开启loading
    if (!config.hideloading) {
      // loading
      Toast.loading({
        forbidClick: true
      })
    }
    if (store.getters.token) {
      config.headers['X-Token'] = ''
    }
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)
// respone拦截器
service.interceptors.response.use(
  response => {
    Toast.clear()
    const res = response.data
    if (res.status && res.status !== 200) {
      // 登录超时,重新登录
      if (res.status === 401) {
        store.dispatch('FedLogOut').then(() => {
          location.reload()
        })
      }
      return Promise.reject(res || 'error')
    } else {
      return Promise.resolve(res)
    }
  },
  error => {
    Toast.clear()
    console.log('err' + error) // for debug
    return Promise.reject(error)
  }
)
export default service
```

sunnie committed
405 406
#### 接口管理

sunnie committed
407 408 409 410 411
`src/api` 文件夹下统一管理接口

- 你可以建立多个模块对接接口, 比如 `home.js` 里是首页的接口这里讲解 `user.js`
- `url` 接口地址,请求的时候会拼接上 `config` 下的 `baseApi`
- `method` 请求方法
sunnie committed
412 413
- `data` 请求参数 `qs.stringify(params)` 是对数据系列化操作
- `hideloading` 默认 `false`,设置为 `true` 后,不显示 loading ui 交互中有些接口不需要样用户感知
sunnie committed
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431

```javascript
import qs from 'qs'
// axios
import request from '@/utils/request'
//user api

// 登录
export function login(params) {
  return request({
    url: '/user/login', // 接口地址
    method: 'post', //  method
    data: qs.stringify(params)
    // hideloading: true
  })
}
```

sunnie committed
432 433 434 435 436 437 438 439 440 441 442 443
#### 如何调用

```javascript
// 请求接口
import {getUserInfo} from '@/api/user.js'

const params = {user: 'sunnie'}
getUserInfo()
  .then(() => {})
  .catch(() => {})
```

sunnie committed
444 445
[▲ 回顶部](#top)

sunnie committed
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484
### <span id="base">✅ vue.config.js 基础配置 </span>

如果你的 `Vue Router` 模式是 hash

```javascript
publicPath: './',
```

如果你的 `Vue Router` 模式是 history 这里的 publicPath 和你的 `Vue Router` `base` **保持一直**

```javascript
publicPath: '/app/',
```

```javascript
const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)

module.exports = {
  publicPath: './', // 署应用包时的基本 URL。 vue-router hash 模式使用
  //  publicPath: '/app/', // 署应用包时的基本 URL。  vue-router history模式使用
  outputDir: 'dist', //  生产环境构建文件的目录
  assetsDir: 'static', //  outputDir的静态资源(js、css、img、fonts)目录
  lintOnSave: false,
  productionSourceMap: !IS_PROD, // 生产环境的 source map
  devServer: {
    port: 9020, // 端口号
    open: false, // 启动后打开浏览器
    overlay: {
      //  当出现编译器错误或警告时,在浏览器中显示全屏覆盖层
      warnings: false,
      errors: true
    }
    // ...
  }
}
```

[▲ 回顶部](#top)

宋楠 committed
485
### <span id="proxy">✅ 配置 proxy 跨域 </span>
sunnie committed
486

sunnie committed
487 488 489 490
如果你的项目需要跨域设置,你需要打来 `vue.config.js` `proxy` 注释 并且配置相应参数

**注意**:你还需要将 `src/config/env.development.js` 里的 `baseApi` 设置成 '/'

sunnie committed
491 492 493 494 495 496 497 498
```javascript
module.exports = {
  devServer: {
    // ....
    proxy: {
      //配置跨域
      '/api': {
        target: 'https://test.xxx.com', // 接口的域名
sunnie committed
499
        // ws: true, // 是否启用websockets
sunnie committed
500 501 502 503 504 505 506 507 508 509
        changOrigin: true, // 开启代理,在本地创建一个虚拟服务端
        pathRewrite: {
          '^/api': '/'
        }
      }
    }
  }
}
```

sunnie committed
510
使用 例如: `src/api/home.js`
sunnie committed
511 512

```javascript
sunnie committed
513 514 515 516 517 518 519
export function getUserInfo(params) {
  return request({
    url: '/api/userinfo',
    method: 'get',
    data: qs.stringify(params)
  })
}
sunnie committed
520 521 522 523
```

[▲ 回顶部](#top)

宋楠 committed
524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665
### <span id="alias">✅ 配置 alias 别名 </span>

```javascript
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 => {
    // 添加别名
    config.resolve.alias
      .set('@', resolve('src'))
      .set('assets', resolve('src/assets'))
      .set('api', resolve('src/api'))
      .set('views', resolve('src/views'))
      .set('components', resolve('src/components'))
  }
};
```
[▲ 回顶部](#top)

### <span id="bundle">✅ 配置 打包分析 </span>

```javascript
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
  .BundleAnalyzerPlugin;

module.exports = {
  chainWebpack: config => {
    // 打包分析
    if (IS_PROD) {
      config.plugin("webpack-report").use(BundleAnalyzerPlugin, [
        {
          analyzerMode: "static"
        }
      ]);
    }
  }
};
```
```bash
npm run build
```
[▲ 回顶部](#top)

### <span id="proxy">✅ 配置 externals 引入 cdn 资源 </span>

```javascript
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
const cdn = {
  css: ['https://cdn.jsdelivr.net/npm/vant@beta/lib/index.css'],
  js: [
    'https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js',
    'https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.6/vue-router.min.js',
    'https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js',
    'https://cdnjs.cloudflare.com/ajax/libs/vuex/3.1.1/vuex.min.js',
    'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js',
    'https://cdn.jsdelivr.net/npm/vant@beta/lib/vant.min.js'
  ]
}
module.exports = {
  configureWebpack: config => {
   config.name = name
    // 为生产环境修改配置...
    if (IS_PROD) {
      // externals
      config.externals = externals
    };
  },
  chainWebpack: config => {
    // 添加CDN参数到htmlWebpackPlugin配置中, 详见public/index.html 修改
    config.plugin('html').tap(args => {
      if (IS_PROD) {
         // html中添加cdn
        args[0].cdn = cdn
      } 
      return args
    })
  }
};
```
 在 public/index.html 中添加

```javascript
    <!-- 使用CDNCSS文件 --> 
    <% for (var i in
      htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.css) { %>
      <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet" />
    <% } %>
     <!-- 使用CDN加速的JS文件,配置在vue.config.js -->
    <% for (var i in
      htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
      <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
    <% } %>
```

[▲ 回顶部](#top)

### <span id="console">✅ 去掉 console.log </span>

```bash
npm i -D babel-plugin-transform-remove-console
```
在 babel.config.js 中配置

```javascript
// 获取 VUE_APP_ENV 非 NODE_ENV,测试环境依然 console
const IS_PROD = ['production', 'prod'].includes(process.env.VUE_APP_ENV)
const plugins = [
  [
    'import',
    {
      libraryName: 'vant',
      libraryDirectory: 'es',
      style: true
    },
    'vant'
  ]
]
// 去除 console.log
if (IS_PROD) {
  plugins.push('transform-remove-console')
}

module.exports = {
  presets: [['@vue/cli-plugin-babel/preset', {useBuiltIns: 'entry'}]],
  plugins
}

```
sunnie committed
666

宋楠 committed
667 668
[▲ 回顶部](#top)

宋楠 committed
669 670 671 672 673 674 675 676 677
### <span id="ie">✅ 添加 IE 兼容 </span>

[▲ 回顶部](#top)

### <span id="console">✅ 去掉 console.log </span>
[▲ 回顶部](#top)



sunnie committed
678
#### 总结
sunnie committed
679

406803045 committed
680
因为项目刚刚构建起来,后面还会持续更新,实际使用过程中一定还有很多问题,如果文章中有错误希望能够被指正,一起成长
sunnie committed
681

406803045 committed
682
# 关于我
sunnie committed
683

sunnie committed
684 685 686
获取更多技术相关文章,关注公众号”前端女塾“。

回复加群,即可加入”前端仙女群“
sunnie committed
687

sunnie committed
688 689 690 691 692
 <p>
  <img src="./static/gognzhonghao.jpg" width="256" style="display:inline;">
</p>
扫描添加下方的微信并备注 Sol 加交流群,交流学习,及时获取代码最新动态。

406803045 committed
693
<p>
sunnie committed
694
  <img src="./static/me.png" width="256" style="display:inline;">
406803045 committed
695 696
</p>
 
sunnie committed
697
如果对你有帮助送我一颗小星星(づ ̄3 ̄)づ╭❤~