koa@2學習筆記

前言:站在巨人的肩膀上,感謝前輩們的付出與貢獻

安裝 koa 模塊

koa 需要 node v7.6.0 及以上版本,提供 ES6 和 async 函數(shù)支持

$ npm install koa

新建 hello.js

const Koa = require('koa');
const app = new Koa();

// response
app.use(ctx => {
  ctx.body = 'Hello Koa';
});

app.listen(3000);

中間件

普通函數(shù)

app.use((ctx, next) => {
  const start = Date.now();
  return next().then(() => {
    const ms = Date.now() - start;
    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
  });
});

async 函數(shù)(node v7.6.0+)

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

中間件開發(fā)

/* ./middleware/logger-async.js */

function log( ctx ) {
    console.log( ctx.method, ctx.header.host + ctx.url )
}

module.exports = function () {
  return async function ( ctx, next ) {
    log(ctx);
    await next()
  }
}
/* index.js */
const Koa = require('koa')
const loggerAsync = require('./middleware/logger-async')
var app = new Koa()

app.use(loggerAsync())

app.use((ctx) => {
    ctx.body = 'hello world'
})
app.listen(3000, 'localhost', () => {
    console.log('starting on port: ', 3000)
})
//控制臺
PS D:\workspace\koa2demo> node .\index.js
starting on port:  3000
GET localhost:3000/

理解 async/await

function getSyncTime() {
    return new Promise((resolve, reject) => {
        try {
            let startTime = new Date().getTime()
            setTimeout(() => {
                let endTime = new Date().getTime()
                let data = endTime - startTime
                resolve(data)
            }, 500)
        } catch (err) {
            reject(err)
        }
    })
}
async function getSyncData() {
    let time = await getSyncTime()
    let data = `endTime - startTime = ${time}`
    return data
} async function getData() {
    let data = await getSyncData()
    console.log(data)
}
getData()
async/await

koa2特性

  • 利用ES7的async/await的來處理傳統(tǒng)回調(diào)嵌套問題和代替koa@1的generator
  • 中間件只支持 async/await 封裝,如果要使用koa@1基于generator中間件,需要通過中間件koa-convert封裝一下才能使用

路由中間件 koa-router

npm install koa-router --save
/* index.js */
const Koa = require('koa')
const fs = require('fs')
const app = new Koa()

const Router = require('koa-router')

//子路由1
let home = new Router()
home.get('/', async (ctx) => {
    let html = `
        <ul>
            <li><a href="/page/helloworld">/page/helloworld</a></li>
            <li><a href="/page/404">/page/404</a></li>
        </ul>
    `
    ctx.body = html
})

//子路由2
let page = new Router()
page
    .get('/404', async (ctx) => {
        ctx.body = '404 page'
    })
    .get('/helloworld', async (ctx) => {
        ctx.body = 'helloworld page'
    })

//裝載所有子路由
let router = new Router()
router.use('/', home.routes(), home.allowedMethods())
router.use('/page', page.routes(), page.allowedMethods())

//加載路由中間件
app.use(router.routes()).use(router.allowedMethods())

app.listen(3000, () => {
    console.log('[demo] koa-router is starting on port: 3000')
})
//console
PS D:\workspace\koa2demo> node .\index.js
[demo] koa-router is starting on port: 3000

請求獲取數(shù)據(jù)

GET請求

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx)=>{
    let url = ctx.url
    //從上下文的request對象中獲取
    let request = ctx.request
    let req_query = request.query
    let req_querystring = request.querystring

    //從上下文直接獲取
    let ctx_query = ctx.query
    let ctx_querystring = ctx.querystring

    ctx.body = {
        url,
        req_query,
        req_querystring,
        ctx_query,
        ctx_querystring
    }
})

app.listen(3000, () => {
    console.log('[demo] get request is starting on port: 3000')
})
GET

POST請求獲取數(shù)據(jù)

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx) => {
    if (ctx.url === '/' && ctx.method === 'GET') {
        //get請求時返回表單
        let html = `
            <h2>koa@2 post request</h2>
            <form action="/" method="POST">
                <p>userName</p>
                <input name="username" type="text"><br>
                <p>userPwd</p>
                <input name="userPwd" type="password"><br>
                <button type="submit">submit</button>
            </form>
        `
        ctx.body = html
    } else if (ctx.url === '/' && ctx.method === 'POST') {
        //post請求時,解析表單里數(shù)據(jù),并顯示
        let postData = await parsePostData(ctx)
        ctx.body = postData
    } else {
        //其他請求顯示404
        ctx.body = '<h1>404 page</h1>'
    }
})

//解析上下文里node原生請求的post參數(shù)
function parsePostData(ctx) {
    return new Promise((resolve, reject) => {
        try {
            let postData = '';
            ctx.req.addListener('data', (data) => {
                postData += data
            })
            ctx.req.addListener('end', () => {
                let parseData = parseQueryStr(postData)
                resolve(parseData)
            })
        } catch (err) {
            reject(err)
        }
    })
}

//將post請求參數(shù)字符串解析成JSON
function parseQueryStr(queryStr) {
    let queryData = {}
    let queryStrList = queryStr.split('&')
    console.log(queryStrList)
    for (let [index, queryStr] of queryStrList.entries()) {
        let itemList = queryStr.split('=')
        queryData[itemList[0]] = decodeURIComponent(itemList[1])
    }
    return queryData
}

app.listen(3000, () => {
    console.log('[demo] post request is starting on port: 3000')
})
POST表單請求 請求響應結(jié)果
POST表單
提交結(jié)果

koa-bodyparser 中間件

const Koa = require('koa')
const app = new Koa()
const bodyParser = require('koa-bodyparser')

//使用ctx.body解析中間件
app.use(bodyParser())

app.use(async (ctx) => {
    if (ctx.url === '/' && ctx.method === 'GET') {
        //get請求時返回表單
        let html = `
            <h2>koa@2 post request</h2>
            <form action="/" method="POST">
                <p>userName</p>
                <input name="username" type="text"><br>
                <p>userPwd</p>
                <input name="userPwd" type="password"><br>
                <button type="submit">submit</button>
            </form>
        `
        ctx.body = html
    } else if (ctx.url === '/' && ctx.method === 'POST') {
        //post請求時,解析表單里數(shù)據(jù),并顯示
        let postData = ctx.request.body
        ctx.body = postData
    } else {
        //其他請求顯示404
        ctx.body = '<h1>404 page</h1>'
    }
})

app.listen(3000, () => {
    console.log('[demo] koa-bodyparser is starting on port: 3000')
})

靜態(tài)資源加載

koa-static中間件

const Koa = require('koa')
const path = require('path')
const static = require('koa-static')

const app = new Koa()

//靜態(tài)資源相對路徑
const staticPath = './public'

app.use(static(path.join(__dirname, staticPath)))

app.use(async (ctx) => {
    ctx.body = 'hello koa@2'
})

app.listen(3000, () => {
    console.log('[demo] koa-static middleware is starting on port: 3000')
})

koa2使用cookie

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx) => {
    if (ctx.url === '/index') {
        ctx.cookies.set('cid', 'hello world', {
            domain: 'localhost',//cookie所在的域名
            path: '/index',//cookie所在的路徑
            maxAge: 20 * 60 * 1000,//cookie有效時長
            expires: new Date('2018-10-24'),//cookie失效時間
            httpOnly: false,//是否只用于http請求中獲取
            overwrite: false//是否允許重寫
        })
        ctx.body = 'cookie is ok'
    } else {
        ctx.body = 'hello koa@2'
    }
})

app.use(async (ctx) => {
    ctx.body = 'hello koa@2'
})

app.listen(3000, () => {
    console.log('[demo] cookie is starting on port: 3000')
})
cookie

koa2實現(xiàn)session

存放mysql中

//創(chuàng)建mysql數(shù)據(jù)庫名為koademo
CREATE DATABASE IF NOT EXISTS koademo DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
const Koa = require('koa')
const session = require('koa-session-minimal')
const MysqlSession = require('koa-mysql-session')

const app = new Koa()

//配置存儲session信息的mysql
let store = new MysqlSession({
    user: 'root',
    password: 'root',
    database: 'koademo',
    host: '127.0.0.1'
})

//存放sessionId的cookie配置
let cookie = {
    maxAge: '',
    expires: '',
    path: '',
    domain: '',
    httpOnly: '',
    overwrite: '',
    secure: '',
    sameSite: '',
    signed: ''
}

//使用session中間件
app.use(session({
    key: 'SESSION_ID',
    store: store,
    cookie: cookie
}))

app.use(async (ctx) => {
    //設(shè)置session
    if (ctx.url === '/set') {
        ctx.session = {
            user_id: Math.random().toString(36).substr(2),
            count: 0
        }
        ctx.body = ctx.session
    } else if (ctx.url === '/') {
        //讀取session信息
        ctx.session.count = ctx.session.count + 1
        ctx.body = ctx.session
    }
})

app.listen(3000, () => {
    console.log('[demo] session is starting on port: 3000')
})

加載模板引擎

koa-views中間件

const Koa = require('koa')
const views = require('koa-views')
const path = require('path')

const app = new Koa()

//加載模板引擎
app.use(views(path.join(__dirname, './views'), {
    extension: 'ejs'
}))

app.use(async (ctx) => {
    let title = 'hello koa@2'
    await ctx.render('index', {
        title
    })
})

app.listen(3000, () => {
    console.log('[demo] koa-views ejs is starting on port: 3000')
})

文件上傳

busboy模塊

busboy模塊是用來解析post請求,node原生req中的文件流

const inspect = require('util').inspect
const path = require('path')
const fs = require('fs')
const Busboy = require('busboy')

//req為node原生請求
const busboy = new Busboy({ headers: req.headers })

//監(jiān)聽文件解析事件
busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
    console.log(`File [${fieldname}]: filename: ${filename}`)

    //文件保存特定路徑
    file.pipe(fs.createWriteStream('./upload'))

    //開始解析文件流
    file.on('data', (data) => {
        console.log(`File [${fieldname}] got ${data.length} bytes`)
    })

    //解析文件結(jié)束
    file.on('end', () => {
        console.log(`File [${fieldname}] finished`)
    })
})

//監(jiān)聽請求中的字段
busboy.on('field', (fieldname, val, fieldnameTruncated, valTruncated) => {
    console.log(`Field [${fieldname}]: value: ${inspect(val)}`)
})

//監(jiān)聽結(jié)束事件
busboy.on('finish', () => {
    console.log('Done parsing form!')
    res.writeHead(303, { Connection: 'close', Location: '/' })
    res.end()
})
req.pipe(busboy)

上傳文件簡單實現(xiàn)

封裝上傳文件到寫入服務方法

const inspect = require('util').inspect
const path = require('path')
const os = require('os')
const fs = require('fs')
const Busboy = require('busboy')

/**
* 同步創(chuàng)建文件目錄
* @param  {string} dirname 目錄絕對地址
* @return {boolean}        創(chuàng)建目錄結(jié)果
*/ function mkdirsSync(dirname) {
    if (fs.existsSync(dirname)) {
        return true
    } else {
        if (mkdirsSync(path.dirname(dirname))) {
            fs.mkdirSync(dirname)
            return true
        }
    }
}

/**
* 獲取上傳文件的后綴名
* @param  {string} fileName 獲取上傳文件的后綴名
* @return {string}          文件后綴名
*/
function getSuffixName(fileName) {
    let nameList = fileName.split('.')
    return nameList[nameList.length - 1]
}

     /**
* 上傳文件
* @param  {object} ctx     koa上下文
* @param  {object} options 文件上傳參數(shù) fileType文件類型, path文件存放路徑
* @return {promise}         
*/ function uploadFile(ctx, options) {
    let req = ctx.req
    let res = ctx.res
    let busboy = new Busboy({ headers: req.headers }) // 獲取類型 
    let fileType = options.fileType || 'common'
    let filePath = path.join(options.path, fileType)
    let mkdirResult = mkdirsSync(filePath)
    return new Promise((resolve, reject) => {
        console.log('文件上傳中...')
        let result = { success: false, formData: {}, } // 解析請求文件事件 
        busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
            let fileName = Math.random().toString(16).substr(2) + '.' + getSuffixName(filename)
            let _uploadFilePath = path.join(filePath, fileName)
            let saveTo = path.join(_uploadFilePath) // 文件保存到制定路徑 
            file.pipe(fs.createWriteStream(saveTo)) // 文件寫入事件結(jié)束 
            file.on('end', function () {
                result.success = true
                result.message = '文件上傳成功'
                console.log('文件上傳成功!')
                resolve(result)
            })
        }) // 解析表單中其他字段信息 
        busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
            console.log('表單字段數(shù)據(jù) [' + fieldname + ']: value: ' + inspect(val));
            result.formData[fieldname] = inspect(val);
        }); // 解析結(jié)束事件 
        busboy.on('finish', function () {
            console.log('文件上結(jié)束')
            resolve(result)
        }) // 解析錯誤事件 
        busboy.on('error', function (err) {
            console.log('文件上出錯')
            reject(result)
        })
        req.pipe(busboy)
    })
}

module.exports = { uploadFile }

入口文件

const Koa = require('koa')
const path = require('path')
const app = new Koa()
// const bodyParser = require('koa-bodyparser')
const { uploadFile } = require('./util/upload')
// app.use(bodyParser()) 
app.use(async (ctx) => {
    if (ctx.url === '/' && ctx.method === 'GET') {
        // 當GET請求時候返回表單頁面
        let html = `
            <h1>koa2 upload demo</h1>
            <form method="POST" action="/upload.json" enctype="multipart/form-data">
            <p>file upload</p>
            <span>picName:</span><input name="picName" type="text" /><br/>
            <input name="file" type="file" /><br/><br/>
            <button type="submit">submit</button>
            </form>
        `
        ctx.body = html
    } else if (ctx.url === '/upload.json' && ctx.method === 'POST') {
        // 上傳文件請求處理
        let result = {
            success: false
        }
        let serverFilePath = path.join(__dirname, 'upload-files')
        // 上傳文件事件
        result = await uploadFile(ctx, {
            fileType: 'album', // common or album 
            path: serverFilePath
        })
        ctx.body = result
    } else {
        // 其他請求顯示404 
        ctx.body = '<h1>404?。?! o(╯□╰)o</h1>'
    }
})

app.listen(3000, () => {
    console.log('[demo] upload-simple is starting at port 3000')
})

異步上傳圖片

入口文件
const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const convert = require('koa-convert')
const static = require('koa-static')
const { uploadFile } = require('./util/upload')
const app = new Koa()

/**
* 使用第三方中間件 start 
*/
app.use(views(path.join(__dirname, './views'), {
    extension: 'ejs'
}))

// 靜態(tài)資源目錄對于相對入口文件index.js的路徑 
const staticPath = './public'
// 由于koa-static目前不支持koa2 
// 所以只能用koa-convert封裝一下 
app.use(convert(static(path.join(__dirname, staticPath))))

/**
* 使用第三方中間件 end 
*/
app.use(async (ctx) => {
    if (ctx.method === 'GET') {
        let title = 'upload pic async'
        await ctx.render('index', {
            title,
        })
    }
    else if (ctx.url === '/api/picture/upload.json' && ctx.method === 'POST') {
        // 上傳文件請求處理 
        let result = { success: false }
        let serverFilePath = path.join(__dirname, 'public/image')
        // 上傳文件事件 
        result = await uploadFile(ctx, {
            fileType: 'album',
            path: serverFilePath
        })
        ctx.body = result
    } else {
        // 其他請求顯示404
        ctx.body = '<h1>404!??! o(╯□╰)o</h1>'
    }
})

app.listen(3000, () => {
    console.log('[demo] upload-async is starting at port 3000')
})
上傳圖片流寫操作
const inspect = require('util').inspect
const path = require('path')
const os = require('os')
const fs = require('fs')
const Busboy = require('busboy')

/**
* 同步創(chuàng)建文件目錄
* @param  {string} dirname 目錄絕對地址
* @return {boolean}        創(chuàng)建目錄結(jié)果
*/
function mkdirsSync(dirname) {
    if (fs.existsSync(dirname)) {
        return true
    } else {
        if (mkdirsSync(path.dirname(dirname))) {
            fs.mkdirSync(dirname)
            return true
        }
    }
}

/**
* 獲取上傳文件的后綴名
* @param  {string} fileName 獲取上傳文件的后綴名
* @return {string}          文件后綴名
*/ function getSuffixName(fileName) {
    let nameList = fileName.split('.')
    return nameList[nameList.length - 1]
}

/**
* 上傳文件
* @param  {object} ctx     koa上下文
* @param  {object} options 文件上傳參數(shù) fileType文件類型, path文件存放路徑
* @return {promise}         
*/ function uploadFile(ctx, options) {
    let req = ctx.req
    let res = ctx.res
    let busboy = new Busboy({ headers: req.headers })
    // 獲取類型 
    let fileType = options.fileType || 'common'
    let filePath = path.join(options.path, fileType)
    let mkdirResult = mkdirsSync(filePath)
    return new Promise((resolve, reject) => {
        console.log('文件上傳中...')
        let result = { success: false, message: '', data: null }
        // 解析請求文件事件
        busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
            let fileName = Math.random().toString(16).substr(2) + '.' + getSuffixName(filename)
            let _uploadFilePath = path.join(filePath, fileName)
            let saveTo = path.join(_uploadFilePath)
            // 文件保存到制定路徑 
            file.pipe(fs.createWriteStream(saveTo))
            // 文件寫入事件結(jié)束 
            file.on('end', function () {
                result.success = true
                result.message = '文件上傳成功'
                result.data = {
                    pictureUrl: `//${ctx.host}/image/${fileType}/${fileName}`
                }
                console.log('文件上傳成功!')
                resolve(result)
            })
        })
        // 解析結(jié)束事件 
        busboy.on('finish', function () {
            console.log('文件上結(jié)束')
            resolve(result)
        })
        // 解析錯誤事件 
        busboy.on('error', function (err) {
            console.log('文件上出錯')
            reject(result)
        })
        req.pipe(busboy)
    })
}

module.exports = { uploadFile }
前端代碼
<!DOCTYPE html>
<html lang="en">
    <head>
        <title><%= title%></title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <body>
        <button class="btn" id="J_UploadPictureBtn">上傳圖片</button>
        <hr/>
        <p>上傳進度<span id="J_UploadProgress">0</span>%</p>
        <p>上傳結(jié)果圖片</p>
        <div id="J_PicturePreview" class="preview-picture"> </div> 
        <script src="js/index.js"></script>
    </body>
</html>

上傳操作代碼

(function () {
    let btn = document.getElementById('J_UploadPictureBtn')
    let progressElem = document.getElementById('J_UploadProgress')
    let previewElem = document.getElementById('J_PicturePreview')
    btn.addEventListener('click', function () {
        uploadAction({
            success: function (result) {
                console.log(result)
                if (result && result.success && result.data && result.data.pictureUrl) {
                    previewElem.innerHTML = '![](' + result.data.pictureUrl + ')'
                }
            },
            progress: function (data) {
                if (data && data * 1 > 0) {
                    progressElem.innerText = data
                }
            }
        })
    })

    /**
    * 類型判斷
    * @type {Object}
    */
    let UtilType = {
        isPrototype: function (data) {
            return Object.prototype.toString.call(data).toLowerCase();
        }, isJSON: function (data) {
            return this.isPrototype(data) === '[object object]';
        }, isFunction: function (data) {
            return this.isPrototype(data) === '[object function]';
        }
    }

    /**
    * form表單上傳請求事件
    * @param  {object} options 請求參數(shù)
    */
    function requestEvent(options) {
        try {
            let formData = options.formData
            let xhr = new XMLHttpRequest()
            xhr.onreadystatechange = function () {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    options.success(JSON.parse(xhr.responseText))
                }
            }
            xhr.upload.onprogress = function (evt) {
                let loaded = evt.loaded
                let tot = evt.total
                let per = Math.floor(100 * loaded / tot)
                options.progress(per)
            }
            xhr.open('post', '/api/picture/upload.json')
            xhr.send(formData)
        } catch (err) { options.fail(err) }
    }
/**
* 上傳事件
* @param  {object} options 上傳參數(shù)      
*/ function uploadEvent(options) {
        let file
        let formData = new FormData()
        let input = document.createElement('input')
        input.setAttribute('type', 'file')
        input.setAttribute('name', 'files')
        input.click()
        input.onchange = function () {
            file = input.files[0]
            formData.append('files', file)
            requestEvent({ formData, success: options.success, fail: options.fail, progress: options.progress })
        }
    }

    /**
    * 上傳操作
    * @param  {object} options 上傳參數(shù)     
    */
    function uploadAction(options) {
        if (!UtilType.isJSON(options)) {
            console.log('upload options is null')
            return
        }
        let _options = {}
        _options.success = UtilType.isFunction(options.success) ? options.success : function () { }
        _options.fail = UtilType.isFunction(options.fail) ? options.fail : function () { }
        _options.progress = UtilType.isFunction(options.progress) ? options.progress : function () { }
        uploadEvent(_options)
    }
})()

創(chuàng)建mysql數(shù)據(jù)庫連接池

const mysql = require('mysql')

//創(chuàng)建數(shù)據(jù)連接池
const pool = mysql.createPool({
    host: '127.0.0.1',
    user: 'root',
    password: 'root',
    database: 'koademo'
})

//在數(shù)據(jù)池中進行會話操作
pool.getConnection((err, conn) => {
    conn.query('SELECT * FROM test', (err, rs, fields) => {
        //結(jié)束會話
        conn.release()

        if (err) throw err
    })
})

async/await封裝使用mysql

/* ./async-db.js */
const msyql = require('mysql')
const pool = msyql.createPool({
    host: '127.0.0.1',
    user: 'root',
    password: 'root',
    database: 'koademo'
})

let query = (sql, values) => {
    return new Promise((resolve, reject) => {
        pool.getConnection((err, conn) => {
            if (err) {
                reject(err)
            } else {
                conn.query(sql, values, (err, rows) => {
                    if (err) {
                        reject(err)
                    } else {
                        resolve(rows)
                    }
                    conn.release()
                })
            }
        })
    })
}

module.exports = {
    query
}
/* index.js */
const { query } = require('./async-db')

async function selectAllData() {
    let sql = 'SELECT * FROM test'
    let dataList = await query(sql)
    return dataList
}

async function getData() {
    let dataList = await selectAllData()
    console.log(dataList)
}

getData()

jsonp

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx) => {
    //如果JSONP的請求為GET
    if (ctx.method === 'GET' && ctx.url.split('?')[0] === '/getData.jsonp') {
        //獲取JSONP的callback
        let callbackName = ctx.query.callback || 'callback'
        let returnData = {
            success: true,
            data: {
                text: 'this is a jsonp api',
                time: new Date().getTime()
            }
        }
        //JSONP的script字符串
        let jsonpStr = `;${callbackName}(${JSON.stringify(returnData)})`
        //用text/javascript,讓請求支持跨域請求
        ctx.type = 'text/javascript'
        //輸出jsonp字符串
        ctx.body = jsonpStr
    } else {
        ctx.body = 'hello jsonp'
    }
})
app.listen(3000, () => {
    console.log('[demo] jsonp is tarting on port 3000')
})

koa-jsonp中間件

const Koa = require('koa')
const jsonp = require('koa-jsonp')
const app = new Koa()

//使用中間件
app.use(jsonp())

app.use(async (ctx) => {
    let returnData = {
        success: true,
        data: {
            text: 'this is a jsonp api',
            time: new Date().getTime()
        }
    }
    //直接輸出json
    ctx.body = returnData
})
app.listen(3000, () => {
    console.log('[demo] koa-jsonp is tarting on port 3000')
})

各章節(jié)代碼存放在對應的分支中:所有源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容