Node.js项目(一)

0.前言

  • 架构:前端(Frontend)、后端(Backend)

  • 前端工程化环境:webpack
  • CSS预处理工具:sass
  • JS库(Ajax):jQuery
  • SPA(single page application),路由:SME-Router
  • JS模块化:ES Module,CommonJS Module
  • UI组件库:Bootstrap(AdminLTE)
  • RMVC:art-template

  • Node.js

  • Express

  • MongoDB(Mongoose)


1.webpack工程化

  • webpack作用:对文件进行打包

  • webpack官网:https://webpack.js.org/

  • 初始化:yarn init -y

  • 添加包:yarn add webpack -D , yarn add webpack-cli -D(本项目使用的webpack版本为4.44.2,webpack5.0版本和4.x版本有很大区别,所以建议使用 yarn add webpack@4.44.2 -D yarn add webpack-cli@3.3.12)

  • 查看帮助:webpack --help

  • webpack 是使用文件进行配置的,所以创建一个配置文件:webpack.config.js

开始测试:

webpack.config.js

const path = require('path')

module.exports = {
    //配置入口
    entry: {
        app: './src/app.js'
    },

    //配置出口
    output: {
        //注意必须写物理路径
        path: path.join(__dirname, './dist'),
        filename: 'app.js'

    }
}

src/app.js

console.log(0)

运行:npx webpack

2.原生webpack

  • 添加webpack原生插件:yarn add html-webpack-plugin@4.4.0 -D

webpack.config.js添加插件

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    //...

    //配置插件
    plugins: [
        new HtmlWebpackPlugin({
            template: path.join(__dirname, './public/index.html'),
            filename: 'index.html',
            inject: true
        })

    ]
}

  • 为了方便启动,我们书写一下脚本:
  "scripts": {
    "build":"npx webpack"
  }
  • 使用yarn启动,不用写run:yarn build

3.配置dev-server

  • 安装dev-server:yarn add webpack-dev-server@3.11.2 -D

webpack.config.js

module.exports = {

	//...
    
    //配置dev-server
    devServer: {
        contentBase: path.join(__dirname, 'dist'),
        //是否压缩
        // compress: true,
        port: 8080
    }

}
  • 运行:npx webpack-dev-server -y

  • 为了简化,我们继续写一个脚本: "dev": "npx webpack-dev-server"

4.文件的拷贝

  • 解决favicon.ico不存在的问题:https://www.lagou.com/images/favicon.ico
  • 保存文件至根目录
  • 安装插件:npm i copy-webpack-plugin@6.4.1 -S -D或者yarn add copy-webpack-plugin -S -D
  • webpack.config.js
const CopyPlugin = require("copy-webpack-plugin");

module.exports = {
  plugins: [
    new CopyPlugin({
      patterns: [
        //{ from: "source", to: "dest" },
        //{ from: "other", to: "public" },
           from: path.resolve(__dirname, './public/favicon.ico'),
                    to: 'dist'
      ],
    }),
  ],
};
  • 为了方便,我们统一一下版本:

package.json

{
  "devDependencies": {
    "@webpack-cli/serve": "^1.7.0",
    "copy-webpack-plugin": "6.4.1",
    "html-webpack-plugin": "4.4.1",
    "webpack": "4.41.5",
    "webpack-cli": "3.3.10",
    "webpack-plugin": "^1.0.5"
  },
  "scripts": {
    "build": "npx webpack",
    "dev": "npx webpack-dev-server"
  },
  "dependencies": {
    "webpack-dev-server": "3.10.1"
  },
}
  • 解决devtools的警告:
module.exports = {

	//...
    devtool: 'source-map',
}

  • 总结:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyPlugin = require("copy-webpack-plugin");

module.exports = {

    //配置环境
    mode: 'development',

    devtool: 'source-map',

    //配置入口
    entry: {
        app: './src/app.js'
    },

    //配置出口
    output: {
        //注意必须写物理路径
        path: path.join(__dirname, './dist'),
        filename: 'app.js'

    },
    //配置插件
    plugins: [
        new HtmlWebpackPlugin({
            template: path.join(__dirname, './public/index.html'),
            filename: 'index.html',
            inject: true
        }),
        new CopyPlugin({
            patterns: [
                {
                    from: path.resolve(__dirname, './public/favicon.ico'),
                    to: 'dist'
                },
            ],
        }),

    ],
    //配置dev-server
    devServer: {
        contentBase: path.join(__dirname, 'dist'),
        //是否压缩
        // compress: true,
        port: 8080
    }

}

5.art-template模板

  • 1.引用js,css:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>lagou-admin</title>
    <link rel="stylesheet" href="./libs/css/bootstrap.min.css">
    <!-- Font Awesome -->
    <link rel="stylesheet" href="./libs/css/font-awesome.min.css">
    <!-- Ionicons -->
    <link rel="stylesheet" href="./libs/css/ionicons.min.css">
    <link rel="stylesheet" href="./libs/css/AdminLTE.min.css">
    <link rel="stylesheet" href="./libs/css/skin-blue.min.css">
    <link rel="stylesheet" href="./libs/css/blue.css">
</head>

<body class="hold-transition skin-blue sidebar-mini login-page">
    <div id="root"></div>
    <script src="./libs/js/jquery-2.2.3.min.js"></script>
    <script src="./libs/js/jquery.form.min.js"></script>
    <script src="./libs/js/socket.io.js"></script>
    <script src="./libs/js/bootstrap.min.js"></script>
    <script src="./libs/js/app.min.js"></script>
    <script src="./libs/js/icheck.min.js"></script>
</body>

</html>
  • 2.引入art-template

npm i art-template art-template-loader -S -D

(也可以使用yarn安装)

  • 3.添加新的配置项:

webpack.config.js

//这个插件是用来每次打包前先清除原来的内容
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
	//...

    //配置loaders
    module: {
        rules: [
            {
                test: /\.art$/,
                //排除部分
                exclude: /(node_modules)/,
                use: {
                    loader: 'art-template-loader'
                }

            }
        ]
    },
    
    //配置插件
    plugins: [
		//...
        new CleanWebpackPlugin()

    ],

}
  • index.art是你要准备的网页内容

src/app.js

//es6导入模块的方法
import indexTpl from './views/index.art'

const html = indexTpl({})

$('#root').html(html)

6.路由(sme-router)

import SMERouter from 'sme-router'
//es6导入模块的方法
import indexTpl from '../views/index.art'
import signinTpl from '../views/signin.art'

const router = new SMERouter('root')

const htmlIndex = indexTpl({})
const htmlSignin = signinTpl({})

// $('#root').html(signin)


router.route('/', (req, res, next) => {
    res.render(htmlSignin)

})

router.route('/signin', (req, res, next) => {
    res.render(htmlSignin)
})

export default router

src/app.js

//载入路由
import router from './routers'
router.go('/')

7.分离controller层

src/controller/index.js

//es6导入模块的方法
import router from '../routers'
import indexTpl from '../views/index.art'
import signinTpl from '../views/signin.art'

const htmlIndex = indexTpl({})
const htmlSignin = signinTpl({})
const _handleSubmit = (router) => {
    return (e) => {
        e.preventDefault()
        router.go('/index')

    }
}

const signin = (router) => {
    return (req, res, next) => {
        res.render(htmlSignin)
        $('#signin').on('submit', _handleSubmit(router))

    }
}

const index = (router) => {
    return (req, res, next) => {
        res.render(htmlIndex)

    }
}

export {
    signin,
    index
}

signin.art

<div class="login-box">
  <div class="login-logo">
    <a href="index2.html"><b>拉勾网</b>后台管理系统</a>
  </div>
  <!-- /.login-logo -->
  <div class="login-box-body">
    <p class="login-box-msg">请登录</p>

    <form id="signin" action="">
      <div class="form-group has-feedback">
        <input type="text" name="username" class="form-control" placeholder="用户名">
        <span class="glyphicon glyphicon-envelope form-control-feedback"></span>
      </div>
      <div class="form-group has-feedback">
        <input type="password" name="password" class="form-control" placeholder="密码">
        <span class="glyphicon glyphicon-lock form-control-feedback"></span>
      </div>
      <div class="row">
        <!-- /.col -->
        <div class="col-xs-4">
          <button type="submit" class="btn btn-primary btn-block btn-flat" >登录</button>
        </div>
        <!-- /.col -->
      </div>
    </form>

  </div>
  <!-- /.login-box-body -->
</div>
<!-- /.login-box -->

routers/index.js

import SMERouter from 'sme-router'
const router = new SMERouter('root')


import { signin, index } from '../controller'

// $('#root').html(signin)


router.route('/', signin(router))
router.route('/index', index(router))

router.route('/signin', signin(router))

export default router

8.css loader

  • 安装:yarn add css-loader@5.0.1 style-loader@2.0.0 -D

webpack.config.js


module.exports = {
	//...

    //配置loaders
    module: {
        rules: [
            {
                test: /\.css$/,
             loaders: ['style-loader', 'css-loader']

            }
        ]
    },

}
  • src/assets/common.css
html,
body {
    height: 100%;

}

#root {
    height: 100%;
}
  • controller/index.js
//...
const index = (router) => {
    return (req, res, next) => {
        res.render(htmlIndex)
        //window.resize(),让页面撑满整个屏幕
        $(window, '.wrapper').resize()

    }
}
  • 注意插件的版本号

9.express工程化(backend)

  • 从这里开始步入后端

  • 初始化yarn init -y

  • 添加依赖yarn global add express-generator

  • 初始化express -e

  • 下载依赖:yarn installor npm i

  • 为了方便修改package.json里面的脚本scripts:start:nodemon ./bin/www

  • 启动项目:yarn start

1.修改路由

修改app.js

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

// var indexRouter = require('./routes/index');
// var usersRouter = require('./routes/users');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// app.use('/', indexRouter);
// app.use('/users', usersRouter);
const usersRouter = require('./routes/users')
app.use('/api/users', usersRouter)

// catch 404 and forward to error handler
app.use(function (req, res, next) {
  next(createError(404));
});

// error handler
app.use(function (err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

我们这里不需要用到默认的routes/index.js,所以把它删除掉

修改routes/users.js

var express = require('express');
var router = express.Router();

/* GET users listing. */
router.post('/signup', function (req, res, next) {
  res.send('respond with a resource');
});

module.exports = router;

  • 后端的测试:使用postman或者insomnia测试
    • 测试地址:http://localhost:3000/api/users/signup
    • 请求方式:POST
    • 携带参数:
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AsFQogrJ-1668266731497)(D:\Typora\imgs\image-20220829215138044.png)]

2.controllers

controllers/users.js

const signup = (req, res, next) => {
    res.send('hello')
}

exports.signup = signup

routes/users.js

var express = require('express');
var router = express.Router();

const { signup } = require('../controllers/users')

/* GET users listing. */
router.post('/signup', signup);

module.exports = router;

3.ejs模板

view/succ.ejs

{
"ret": true,
"errorCode": 0,
"data":<%- data %>
    }

controllers/users.js

//注册用户

const signup = (req, res, next) => {

    const { username, password } = req.body

    //这里直接使用模板,第一个参数是模板模板名称succ.ejs
    res.render('succ', {
        data: JSON.stringify({ username, password })
    })
}

exports.signup = signup

4.mongoose

// getting-started.js
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/lagou-admin');
  • 创建数据库:use lagou-admin

  • kittens demo:

    • db.js
    // getting-started.js
    var mongoose = require('mongoose');
    mongoose.connect('mongodb://localhost/lagou-admin', {
        useNewUrlParser: true,
        useUnifiedTopology: true
    });
    
    var db = mongoose.connection;
    db.on('error', console.error.bind(console, 'connection error:'));
    
    //mongonDB里面的schema就相当于一个模型
    var kittySchema = mongoose.Schema({
        name: String
    });
    
    var Kitten = mongoose.model('Kitten', kittySchema);
    
    var felyne = new Kitten({ name: 'Felyne' });
    felyne.save()
    console.log(felyne.name); // 'Felyne'
    
    • controllers/users.js
    require('../utils/db')
    //注册用户
    
    const signup = (req, res, next) => {
    
        const { username, password } = req.body
    
        //这里直接使用模板,第一个参数是模板模板名称succ.ejs
        res.render('succ', {
            data: JSON.stringify({ username, password })
        })
    }
    
    exports.signup = signup
    
    • 运行:yarn start
    • 测试:insomnia:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qzxDqL6Z-1668266731499)(D:\Typora\imgs\image-20220829225000443.png)]

    • 结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pUdXA9zz-1668266731499)(D:\Typora\imgs\image-20220829225020592.png)]

5.构建自己的schema

db.js

//...
//mongonDB里面的schema就相当于一个模型
var usersSchema = mongoose.Schema({
    username: String,
    password: String
});

//集合的名字
var Users = mongoose.model('users', usersSchema);

exports.Users = Users

我们把controller中对db.js的引用删除,提取出M层:model/users.js

const { Users } = require('../utils/db')

const signup = ({ username, password }) => {
    const users = new Users({
        username,
        password
    })
    users.save()
}

//下面两种写法是等价的
// exports.signup = signup
module.exports = {
    signup
}

controllers/users.js

const usersModel = require('../models/users')
//注册用户

const signup = (req, res, next) => {

    const { username, password } = req.body
    usersModel.signup({
        username,
        password
    })

    //这里直接使用模板,第一个参数是模板模板名称succ.ejs
    res.render('succ', {
        data: JSON.stringify({ username, password })
    })
}

exports.signup = signup

6.同异步问题与注册业务逻辑

controllers/users.js

const usersModel = require('../models/users')
//注册用户
const signup = async (req, res, next) => {

    const { username, password } = req.body

    //判断用户是否存在
    let findResult = await usersModel.findUser(username)
    console.log(findResult)
    if (findResult) {
        res.render('fail', {
            data: JSON.stringify({
                message: '用户名已存在'
            })
        })

    } else {
        //数据库里没有这个用户,开始添加用户
        let result = await usersModel.signup({
            username,
            password
        })
        console.log(result)
    }



    //这里直接使用模板,第一个参数是模板模板名称succ.ejs
    res.render('succ', {
        data: JSON.stringify({ username, password })
    })
}

exports.signup = signup

module/users.js

const { Users } = require('../utils/db')

const findUser = (username) => {
    return Users.findOne({ username })

}

const signup = ({ username, password }) => {
    const users = new Users({
        username,
        password
    })
    return users.save()

}

//下面两种写法是等价的
exports.signup = signup
exports.findUser = findUser
// module.exports = {
//     signup
// }

views/fail.ejs

{
"ret": true,
"errorCode": -1,
"data":<%- data %>
    }

7.密码加密-bcrypt

  • 安装:yarn add bcrypt

  • 创建工具js:

utils/tools.js

const bcrypt = require('bcrypt')

exports.hash = (myPlaintextPassword) => {

    return new Promise((resolve, reject) => {
        bcrypt.hash(myPlaintextPassword, 10, function (err, hash) {

            if (err) {
                reject(err)
            }
            resolve(hash)
        })
    });
}

controllers/users.js


const { hash } = require('../utils/tools')
//注册用户
const signup = async (req, res, next) => {
        //设置返回头
    res.set('content-type', 'application/json; charset=utf-8')

    //...
    //加密后的密码
    const bcryptPassword = await hash(password)

    //...
    } else {
        //数据库里没有这个用户,开始添加用户
        let result = await usersModel.signup({
            username,
            password: bcryptPassword
        })

    }
	//...
  • bcryptAPI:
    • hash(data,salt,cb)

    • salt:salt - [REQUIRED] - the salt to be used to hash the password. if specified as a number then a salt will be generated with the specified number of rounds and used

    • rounds=8 : ~40 hashes/sec
      rounds=9 : ~20 hashes/sec
      rounds=10: ~10 hashes/sec
      rounds=11: ~5  hashes/sec
      rounds=12: 2-3 hashes/sec
      rounds=13: ~1 sec/hash
      rounds=14: ~1.5 sec/hash
      rounds=15: ~3 sec/hash
      rounds=25: ~1 hour/hash
      rounds=31: 2-3 days/hash
      

8.跨域访问CORS(backend)

  • 安装插件:yarn add cors

  • app.js中使用中间件

var cors = require('cors')

app.use(cors())

10.跨域访问webpack(frontend)

webpack.config.js


module.exports = {
	//...
    //配置dev-server
    devServer: {
  	//...
        port: 8080,
        proxy: {
            '/api': {
                target: 'http://localhost:3000',
                //pathRewrite: { "^/api": "" }
            }
        }
    }

}
  • 配置完要重启一遍,因为配置文件已经修改了

11.获取用户列表

1.backend

  • 路由:routes/users.js
//...
router.get('/list', list)
  • 控制层:controllers/users.js
//...
//用户列表
const list = async (req, res) => {
        res.set('content-type', 'application/json; charset=utf-8')
    const listResult = await usersModel.findList()
    res.render('succ', {
        data: JSON.stringify( listResult )
    })
}

exports.list = list
  • M层:modules/users.js
//...
const findList = () => {
    //排序
    return Users.find().sort({ _id: -1 })
}

exports.findList = findList

2.frontend

  • views/users-list.art
{{each data}}
<tr>
  <td>{{$index+1}}.</td>
  <td>{{$value.username}}</td>
  <td><button data-id="{{$value._id}}" class="btn btn-danger remove">删除</button></td>
</tr>
{{/each}}
  • controllers/index.js
import usersListTpl from '../views/users-list.art'

//...
const _list = () => {
    $.ajax({
        url: '/api/users/list',
        success(result) {
             $('#users-list').html(usersListTpl({
                data: result.data
            }))
            
            //console.log(result)
        }
    })
}

const index = (router) => {
    return (req, res, next) => {
        //渲染首页
        res.render(htmlIndex)
        //window.resize(),让页面撑满整个屏幕
        $(window, '.wrapper').resize()

        //填充用户列表
        $('#content').html(usersTpl())

        //渲染list
        _list()


        //点击保存,提交表单
        $('#users-save').html(usersTpl())


    }
}

const _signup = () => {
    const $btnClose = $('#users-close')
    //提交表单
    const data = $('#users-form').serialize()
    console.log(data)

    $.ajax({
        url: '/api/users/signup',
        type: 'post',
        data,
        success(res) {
            console.log(res)        
            //刷新
            _list()
        }
    })

}

12.分页(frontend)

  • views/users-pages.art
<ul class="pagination pagination-sm no-margin pull-right" id="users-page-list">
  <li><a href="#">&laquo;</a></li>
  {{each pageArray}}
  <li><a href="#">{{$index+1}}</a></li>
  {{/each}}
  <li><a href="#">&raquo;</a></li>
</ul>
  • controllers/index.js

import userListPageTpl from '../views/users-pages.art'

//...

const pageSize = 5
let dataList = []

const _loadData = () => {
    return $.ajax({
        url: '/api/users/list',
        async: false,
        success(result) {
            dataList = result.data
            //分页
            _pagination(result.data)
        }
    })
}
const _pagination = (data) => {

    const total = data.length
    //往上取整
    const pagesCount = Math.ceil(total / pageSize)
    const pageArray = new Array(pagesCount)

    const htmlPage = userListPageTpl({
        pageArray
    })

    $('#users-page').html(htmlPage)
    //默认添加第一个
    $('#users-page-list li:nth-child(2').addClass('active')
    $('#users-page-list li:not(:first-child , :last-child)').on('click', function () {
        $(this).addClass('active').siblings().removeClass('active')
        // console.log($(this).index())
        _list($(this).index())
    })

}

const _list = (pageNo) => {

    let start = (pageNo - 1) * pageSize
    $('#users-list').html(usersListTpl({
        //截取数据
        data: dataList.slice(start, start + pageSize)
    }))

}


const index = (router) => {
    return async (req, res, next) => {
        //渲染首页
        res.render(htmlIndex)
        //window.resize(),让页面撑满整个屏幕
        $(window, '.wrapper').resize()

        //填充用户列表
        $('#content').html(usersTpl())

        //初次渲染list
        _loadData()
        _list(1)


        //点击保存,提交表单
        $('#users-save').html(usersTpl())

    }
}

const _signup = () => {
    const $btnClose = $('#users-close')

    //提交表单
    const data = $('#users-form').serialize()
    console.log(data)

    $.ajax({
        url: '/api/users/signup',
        type: 'post',
        data,
        sucdess: async (res) => {
            console.log(res)
            //提交数据后渲染
            _loadData()
            _list(1)
        }
    })
    //单击关闭模拟框
    $btnClose.click()

}
//...

13.修改路由

backend/routes/users.js

var express = require('express');
var router = express.Router();

const { signup, list } = require('../controllers/users')

/* GET users listing. */
router.post('/', signup);
router.get('/', list)
// router.delete('/', remove)

module.exports = router;

frontend/controllers/index.js

//es6导入模块的方法
// import router from '../routers'
import indexTpl from '../views/index.art'
import signinTpl from '../views/signin.art'
import usersTpl from '../views/users.art'
import usersListTpl from '../views/users-list.art'
import userListPageTpl from '../views/users-pages.art'


const htmlIndex = indexTpl({})
const htmlSignin = signinTpl({})
const pageSize = 5
let dataList = []
const _handleSubmit = (router) => {
    return (e) => {
        e.preventDefault()
        router.go('/index')

    }
}

const _loadData = () => {
    return $.ajax({
        url: '/api/users',
        async: false,
        success(result) {
            dataList = result.data
            //分页
            _pagination(result.data)
        }
    })
}
const _pagination = (data) => {

    const total = data.length
    //往上取整
    const pagesCount = Math.ceil(total / pageSize)
    const pageArray = new Array(pagesCount)

    const htmlPage = userListPageTpl({
        pageArray
    })

    $('#users-page').html(htmlPage)
    //默认添加第一个
    $('#users-page-list li:nth-child(2').addClass('active')
    $('#users-page-list li:not(:first-child , :last-child)').on('click', function () {
        $(this).addClass('active').siblings().removeClass('active')
        // console.log($(this).index())
        _list($(this).index())
    })

}

const _list = (pageNo) => {

    let start = (pageNo - 1) * pageSize
    $('#users-list').html(usersListTpl({
        //截取数据
        data: dataList.slice(start, start + pageSize)
    }))

}



const signin = (router) => {
    return (req, res, next) => {
        res.render(htmlSignin)
        $('#signin').on('submit', _handleSubmit(router))

    }
}

const index = (router) => {
    return async (req, res, next) => {
        //渲染首页
        res.render(htmlIndex)
        //window.resize(),让页面撑满整个屏幕
        $(window, '.wrapper').resize()

        //填充用户列表
        $('#content').html(usersTpl())

        //初次渲染list
        _loadData()
        _list(1)


        //点击保存,提交表单
        $('#users-save').html(usersTpl())

    }
}

const _signup = () => {
    const $btnClose = $('#users-close')

    //提交表单
    const data = $('#users-form').serialize()
    console.log(data)

    $.ajax({
        url: '/api/users',
        type: 'post',
        data,
        sucdess: async (res) => {
            console.log(res)
            //提交数据后渲染
            _loadData()
            _list(1)
        }
    })
    //单击关闭模拟框
    $btnClose.click()

}

const signup = () => {

}



export {
    signin,
    index
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值