ในโค้ดแล็บนี้ ให้ปรับปรุงประสิทธิภาพของแอปพลิเคชันง่ายๆ นี้ซึ่งอนุญาตให้ผู้ใช้ให้คะแนนแมวแบบสุ่ม ดูวิธีเพิ่มประสิทธิภาพกลุ่ม JavaScript โดยลดจำนวนโค้ดที่เปลี่ยนรูปแบบ
ในแอปตัวอย่าง คุณสามารถเลือกคำหรืออีโมจิเพื่อสื่อว่าคุณชอบแมวแต่ละตัวมากน้อยเพียงใด เมื่อคลิกปุ่ม แอปจะแสดงค่าของปุ่มใต้รูปแมวปัจจุบัน
วัดระยะทาง
คุณควรเริ่มด้วยการตรวจสอบเว็บไซต์ก่อนเพิ่มการเพิ่มประสิทธิภาพเสมอ โดยทำดังนี้
- หากต้องการดูตัวอย่างเว็บไซต์ ให้กดดูแอป แล้วกดเต็มหน้าจอ
- กดแป้น Control+Shift+J (หรือ Command+Option+J ใน Mac) เพื่อเปิดเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์
- คลิกแท็บเครือข่าย
- เลือกช่องทำเครื่องหมายปิดใช้แคช
- โหลดแอปซ้ำ
ใช้พื้นที่เกิน 80 KB สําหรับแอปพลิเคชันนี้ มาดูกันว่ามีการใช้ส่วนใดของแพ็กเกจหรือไม่
กด
Control+Shift+P
(หรือCommand+Shift+P
ใน Mac) เพื่อเปิดเมนูคำสั่งป้อน
Show Coverage
แล้วกดEnter
เพื่อแสดงแท็บความครอบคลุมในแท็บการครอบคลุม ให้คลิกโหลดซ้ำเพื่อโหลดแอปพลิเคชันซ้ำขณะจับภาพการครอบคลุม
ดูจำนวนโค้ดที่ใช้เทียบกับจำนวนที่โหลดสำหรับ Bundle หลัก
มากกว่าครึ่งของ Bundle (44 KB) ไม่ได้ถูกนำมาใช้ เนื่องจากโค้ดจำนวนมากภายในประกอบด้วย polyfill เพื่อให้แอปพลิเคชันทำงานในเบราว์เซอร์รุ่นเก่าได้
ใช้ @babel/preset-env
รูปแบบคำสั่งของภาษา JavaScript เป็นไปตามมาตรฐานที่รู้จักกันในชื่อ ECMAScript หรือ ECMA-262 มีการเผยแพร่ข้อกําหนดเวอร์ชันใหม่ทุกปี ซึ่งจะมีฟีเจอร์ใหม่ที่ผ่านกระบวนการเสนอ เบราว์เซอร์หลักแต่ละเบราว์เซอร์จะรองรับฟีเจอร์เหล่านี้ในขั้นตอนที่แตกต่างกันเสมอ
ฟีเจอร์ ES2015 ต่อไปนี้ใช้ในแอปพลิเคชัน
ระบบจะใช้ฟีเจอร์ ES2017 ต่อไปนี้ด้วย
คุณสามารถดูซอร์สโค้ดใน src/index.js
เพื่อดูวิธีใช้ฟีเจอร์เหล่านี้
Chrome เวอร์ชันล่าสุดรองรับฟีเจอร์เหล่านี้ทั้งหมด แต่เบราว์เซอร์อื่นๆ ที่ไม่รองรับล่ะ Babel ซึ่งรวมอยู่ในแอปพลิเคชันเป็นไลบรารียอดนิยมที่ใช้คอมไพล์โค้ดที่มีไวยากรณ์ใหม่เป็นโค้ดที่เบราว์เซอร์และสภาพแวดล้อมรุ่นเก่าเข้าใจได้ ซึ่งทำได้ 2 วิธีดังนี้
- Polyfill จะรวมอยู่ด้วยเพื่อจําลองฟังก์ชัน ES2015 ขึ้นไปที่ใหม่กว่าเพื่อให้ใช้ API ของฟังก์ชันเหล่านั้นได้แม้ว่าเบราว์เซอร์จะไม่รองรับก็ตาม ต่อไปนี้คือตัวอย่างของ polyfill ของเมธอด
Array.includes
- ปลั๊กอินใช้เพื่อเปลี่ยนรูปแบบโค้ด ES2015 (หรือใหม่กว่า) เป็นไวยากรณ์ ES5 เวอร์ชันเก่า เนื่องจากการเปลี่ยนแปลงเหล่านี้เกี่ยวข้องกับไวยากรณ์ (เช่น ฟังก์ชันลูกศร) จึงไม่สามารถจําลองด้วย polyfill ได้
ดูที่ package.json
เพื่อดูว่า Babel ใดรวมอยู่ด้วย
"dependencies": {
"@babel/polyfill": "^7.0.0"
},
"devDependencies": {
//...
"babel-loader": "^8.0.2",
"@babel/core": "^7.1.0",
"@babel/preset-env": "^7.1.0",
//...
}
@babel/core
คือคอมไพเลอร์หลักของ Babel ซึ่งการกำหนดค่า Babel ทั้งหมดจะกำหนดไว้ใน.babelrc
ที่รูทของโปรเจ็กต์babel-loader
รวม Babel ไว้ในกระบวนการบิลด์ webpack
ตอนนี้ให้ดูที่ webpack.config.js
เพื่อดูว่า babel-loader
รวมอยู่ในกฎอย่างไร
module: { rules: [ //... { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } ] },
@babel/polyfill
มี Polyfill ที่จำเป็นทั้งหมดสำหรับฟีเจอร์ ECMAScript เวอร์ชันใหม่เพื่อให้ทำงานได้ในสภาพแวดล้อมที่ไม่รองรับ ระบบนําเข้าไว้ที่ด้านบนสุดของsrc/index.js.
แล้ว
import "./style.css";
import "@babel/polyfill";
@babel/preset-env
จะระบุการเปลี่ยนรูปแบบและโพลีฟีลที่จำเป็นสำหรับเบราว์เซอร์หรือสภาพแวดล้อมที่เลือกเป็นเป้าหมาย
ดูไฟล์การกําหนดค่า Babel .babelrc
เพื่อดูวิธีรวม
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions"
}
]
]
}
นี่เป็นการตั้งค่า Babel และ webpack ดูวิธีรวม Babel ไว้ในแอปพลิเคชันหากคุณใช้โปรแกรมรวมโมดูลอื่นที่ไม่ใช่ webpack
แอตทริบิวต์ targets
ใน .babelrc
จะระบุเบราว์เซอร์ที่กำหนดเป้าหมาย @babel/preset-env
ผสานรวมกับ browserslist ซึ่งหมายความว่าคุณจะดูรายการการค้นหาทั้งหมดที่เข้ากันได้ซึ่งใช้ในช่องนี้ได้ในเอกสารประกอบ browserslist
ค่า "last 2 versions"
จะแปลงโค้ดในแอปพลิเคชันสำหรับ2 เวอร์ชันล่าสุดของเบราว์เซอร์ทุกรุ่น
การแก้ไขข้อบกพร่อง
หากต้องการดูภาพรวมของเป้าหมาย Babel ทั้งหมดของเบราว์เซอร์ รวมถึงการเปลี่ยนรูปแบบและ polyfill ทั้งหมดที่รวมอยู่ ให้เพิ่มช่อง debug
ลงใน .babelrc:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
}
]
]
}
- คลิกเครื่องมือ
- คลิกบันทึก
โหลดแอปพลิเคชันซ้ำและดูบันทึกสถานะข้อบกพร่องที่ด้านล่างของเครื่องมือแก้ไข
เบราว์เซอร์ที่กำหนดเป้าหมาย
Babel จะบันทึกรายละเอียดบางอย่างลงในคอนโซลเกี่ยวกับกระบวนการคอมไพล์ รวมถึงสภาพแวดล้อมเป้าหมายทั้งหมดที่คอมไพล์โค้ด
โปรดทราบว่าเบราว์เซอร์ที่หยุดให้บริการแล้ว เช่น Internet Explorer จะรวมอยู่ในรายการนี้ ซึ่งทำให้เกิดปัญหาเนื่องจากเบราว์เซอร์ที่ไม่รองรับจะไม่มีการเพิ่มฟีเจอร์ใหม่ๆ และ Babel จะยังคงแปลงไวยากรณ์ที่เฉพาะเจาะจงสำหรับเบราว์เซอร์เหล่านั้นต่อไป ซึ่งจะเพิ่มขนาดของกลุ่มโดยไม่จำเป็นหากผู้ใช้ไม่ได้ใช้เบราว์เซอร์นี้เพื่อเข้าถึงเว็บไซต์
นอกจากนี้ Babel จะบันทึกรายการปลั๊กอินการเปลี่ยนรูปแบบที่ใช้ด้วย
รายการนี้ยาวมาก ปลั๊กอินทั้งหมดที่ Babel ต้องใช้เพื่อเปลี่ยนไวยากรณ์ ES2015 ขึ้นไปเป็นไวยากรณ์เก่าสำหรับเบราว์เซอร์เป้าหมายทั้งหมด
อย่างไรก็ตาม Babel จะไม่แสดง polyfill ที่เฉพาะเจาะจงที่ใช้
เนื่องจากมีการนําเข้า @babel/polyfill
ทั้งหมดโดยตรง
โหลด polyfill ทีละรายการ
โดยค่าเริ่มต้น Babel จะรวม polyfill ทั้งหมดที่จำเป็นสำหรับสภาพแวดล้อม ES2015 ขึ้นไปที่สมบูรณ์เมื่อนําเข้า @babel/polyfill
ไปยังไฟล์ หากต้องการนําเข้าโพลีไฟล์ที่เฉพาะเจาะจงที่จําเป็นสําหรับเบราว์เซอร์เป้าหมาย ให้เพิ่ม useBuiltIns: 'entry'
ลงในการกําหนดค่า
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
"useBuiltIns": "entry"
}
]
]
}
โหลดแอปพลิเคชันซ้ำ ตอนนี้คุณจะเห็น polyfill ที่เฉพาะเจาะจงทั้งหมดที่รวมอยู่ด้วย
แม้ว่าตอนนี้จะมีเฉพาะ polyfill ที่จําเป็นสําหรับ "last 2 versions"
เท่านั้น แต่รายการก็ยังคงยาวมาก เนื่องจากยังคงมีการรวม polyfills ที่จําเป็นสําหรับเบราว์เซอร์เป้าหมายสําหรับฟีเจอร์ที่ใหม่กว่าทุกรายการ เปลี่ยนค่าของแอตทริบิวต์เป็น usage
เพื่อรวมเฉพาะแอตทริบิวต์ที่จำเป็นสำหรับฟีเจอร์ที่ใช้ในโค้ด
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true,
"useBuiltIns": "entry"
"useBuiltIns": "usage"
}
]
]
}
ซึ่งจะรวม polyfills ไว้โดยอัตโนมัติเมื่อจำเป็น
ซึ่งหมายความว่าคุณสามารถนําการนําเข้า @babel/polyfill
ใน src/index.js.
ออกได้
import "./style.css";
import "@babel/polyfill";
ตอนนี้จะมีเฉพาะ polyfill ที่จําเป็นสําหรับแอปพลิเคชันเท่านั้น
ขนาด App Bundle ลดลงอย่างมาก
การจำกัดรายการเบราว์เซอร์ที่รองรับให้แคบลง
จำนวนเป้าหมายเบราว์เซอร์ที่รวมอยู่ยังมีค่อนข้างมาก และผู้ใช้ที่ใช้เบราว์เซอร์ที่หยุดให้บริการแล้ว เช่น Internet Explorer มีจำนวนไม่มาก อัปเดตการกําหนดค่าเป็นค่าต่อไปนี้
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"targets": [">0.25%", "not ie 11"],
"debug": true,
"useBuiltIns": "usage",
}
]
]
}
ดูรายละเอียดของกลุ่มที่ดึงข้อมูล
เนื่องจากแอปพลิเคชันมีขนาดเล็กมาก การเปลี่ยนแปลงเหล่านี้จึงแทบไม่มีความแตกต่างมากนัก อย่างไรก็ตาม เราขอแนะนําให้ใช้เปอร์เซ็นต์ส่วนแบ่งการตลาดของเบราว์เซอร์ (เช่น ">0.25%"
) พร้อมกับยกเว้นเบราว์เซอร์บางรายการที่คุณมั่นใจว่าผู้ใช้ไม่ได้ใช้ อ่านข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้ได้จากบทความของ James Kyle เรื่อง "2 เวอร์ชันล่าสุด" ที่ถือว่าอันตราย
ใช้ <script type="module">
แต่เรายังต้องปรับปรุงอีก แม้ว่าจะมีการนํา Polyfill ที่ไม่ได้ใช้ออกแล้ว แต่ยังมี Polyfill จำนวนมากที่ยังคงมีให้ใช้งานอยู่ซึ่งไม่จําเป็นสําหรับเบราว์เซอร์บางรุ่น การใช้โมดูลจะช่วยให้เขียนไวยากรณ์ใหม่และส่งไปยังเบราว์เซอร์ได้โดยตรงโดยไม่ต้องใช้ polyfill ที่ไม่จำเป็น
โมดูล JavaScript เป็นฟีเจอร์ใหม่ที่กำลังได้รับความนิยมในเบราว์เซอร์หลักๆ ทั้งหมด
คุณสามารถสร้างโมดูลได้โดยใช้แอตทริบิวต์ type="module"
เพื่อกำหนดสคริปต์ที่นำเข้าและส่งออกจากโมดูลอื่นๆ เช่น
// math.mjs
export const add = (x, y) => x + y;
<!-- index.html -->
<script type="module">
import { add } from './math.mjs';
add(5, 2); // 7
</script>
สภาพแวดล้อมที่รองรับโมดูล JavaScript (แทนที่จะต้องใช้ Babel) รองรับฟีเจอร์ ECMAScript เวอร์ชันใหม่หลายรายการอยู่แล้ว ซึ่งหมายความว่าคุณสามารถแก้ไขการกําหนดค่า Babel เพื่อส่งแอปพลิเคชัน 2 เวอร์ชันไปยังเบราว์เซอร์ได้ ดังนี้
- เวอร์ชันที่จะทํางานในเบราว์เซอร์รุ่นใหม่ที่รองรับโมดูล และมีโมดูลที่ไม่มีการแปลงส่วนใหญ่ แต่มีขนาดเล็กกว่า
- เวอร์ชันที่มีสคริปต์ที่แปลงแล้วขนาดใหญ่ขึ้นซึ่งจะใช้งานได้ในเบราว์เซอร์เดิมทุกรุ่น
การใช้ ES Modules กับ Babel
หากต้องการตั้งค่า @babel/preset-env
แยกกันสำหรับแอปพลิเคชัน 2 เวอร์ชัน ให้นำไฟล์ .babelrc
ออก คุณสามารถเพิ่มการตั้งค่า Babel ลงในการกำหนดค่า webpack ได้โดยระบุรูปแบบการคอมไพล์ 2 รูปแบบที่แตกต่างกันสำหรับแอปพลิเคชันแต่ละเวอร์ชัน
เริ่มต้นด้วยการเพิ่มการกําหนดค่าสําหรับสคริปต์เดิมลงใน webpack.config.js
const legacyConfig = {
entry,
output: {
path: path.resolve(__dirname, "public"),
filename: "[name].bundle.js"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
targets: {
esmodules: false
}
}]
]
}
},
cssRule
]
},
plugins
}
โปรดทราบว่าแทนที่จะใช้ค่า targets
สำหรับ "@babel/preset-env"
ระบบจะใช้ esmodules
ที่มีค่าเป็น false
แทน ซึ่งหมายความว่า Babel จะรวมการเปลี่ยนรูปแบบและโพลีฟีลที่จำเป็นทั้งหมดเพื่อกำหนดเป้าหมายไปยังเบราว์เซอร์ทุกรุ่นที่ยังไม่รองรับโมดูล ES
เพิ่มออบเจ็กต์ entry
, cssRule
และ corePlugins
ไว้ที่ส่วนต้นของไฟล์ webpack.config.js
ข้อมูลทั้งหมดเหล่านี้จะแชร์กันระหว่างทั้งโมดูลและสคริปต์เดิมที่แสดงในเบราว์เซอร์
const entry = {
main: "./src"
};
const cssRule = {
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
};
const plugins = [
new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
new HtmlWebpackPlugin({template: "./src/index.html"})
];
ตอนนี้ให้สร้างออบเจ็กต์ config ในทำนองเดียวกันสำหรับสคริปต์โมดูลด้านล่างที่กําหนด legacyConfig
const moduleConfig = {
entry,
output: {
path: path.resolve(__dirname, "public"),
filename: "[name].mjs"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
targets: {
esmodules: true
}
}]
]
}
},
cssRule
]
},
plugins
}
ความแตกต่างหลักๆ ในที่นี้คือจะใช้นามสกุลไฟล์ .mjs
สำหรับชื่อไฟล์เอาต์พุต ค่า esmodules
ได้รับการตั้งค่าเป็น "จริง" ที่นี่ ซึ่งหมายความว่าโค้ดที่แสดงผลในโมดูลนี้เป็นสคริปต์ที่เล็กลงและคอมไพล์น้อยลง ซึ่งไม่ผ่านการเปลี่ยนรูปแบบใดๆ ในตัวอย่างนี้ เนื่องจากเบราว์เซอร์ที่รองรับโมดูลรองรับฟีเจอร์ทั้งหมดที่ใช้แล้ว
ส่งออกการกำหนดค่าทั้ง 2 รายการในอาร์เรย์เดียวที่ส่วนท้ายสุดของไฟล์
module.exports = [
legacyConfig, moduleConfig
];
ตอนนี้ระบบจะสร้างทั้งโมดูลขนาดเล็กสําหรับเบราว์เซอร์ที่รองรับ และสร้างสคริปต์ที่แปลงแล้วขนาดใหญ่สําหรับเบราว์เซอร์รุ่นเก่า
เบราว์เซอร์ที่รองรับโมดูลจะไม่สนใจสคริปต์ที่มีแอตทริบิวต์ nomodule
ในทางกลับกัน เบราว์เซอร์ที่ไม่รองรับโมดูลจะไม่สนใจองค์ประกอบสคริปต์ที่มี type="module"
ซึ่งหมายความว่าคุณสามารถรวมโมดูลและรายการสำรองที่คอมไพล์แล้วได้ โดยแอปพลิเคชันทั้ง 2 เวอร์ชันควรอยู่ในindex.html
ลักษณะนี้
<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>
เบราว์เซอร์ที่รองรับโมดูลจะดึงข้อมูลและเรียกใช้ main.mjs
และละเว้น main.bundle.js.
ส่วนเบราว์เซอร์ที่ไม่รองรับโมดูลจะทำในทางตรงกันข้าม
โปรดทราบว่าสคริปต์โมดูลจะเลื่อนไว้โดยค่าเริ่มต้นเสมอ ซึ่งแตกต่างจากสคริปต์ทั่วไป
หากต้องการให้สคริปต์ nomodule
ที่เทียบเท่าเลื่อนออกไปและดำเนินการหลังจากการแยกวิเคราะห์เท่านั้น คุณจะต้องเพิ่มแอตทริบิวต์ defer
ดังนี้
<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>
ขั้นตอนสุดท้ายที่ต้องทำในที่นี่คือการเพิ่มแอตทริบิวต์ module
และ nomodule
ลงในโมดูลและสคริปต์เดิมตามลำดับ นำเข้า ScriptExtHtmlWebpackPlugin ที่ด้านบนสุดของ webpack.config.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
ตอนนี้อัปเดตอาร์เรย์ plugins
ในการกำหนดค่าให้รวมปลั๊กอินนี้
const plugins = [ new ExtractTextPlugin({filename: "[name].css", allChunks: true}), new HtmlWebpackPlugin({template: "./src/index.html"}), new ScriptExtHtmlWebpackPlugin({ module: /\.mjs$/, custom: [ { test: /\.js$/, attribute: 'nomodule', value: '' }, ] }) ];
การตั้งค่าปลั๊กอินเหล่านี้จะเพิ่มแอตทริบิวต์ type="module"
สำหรับองค์ประกอบสคริปต์ .mjs
ทั้งหมด รวมถึงแอตทริบิวต์ nomodule
สำหรับโมดูลสคริปต์ .js
ทั้งหมด
การแสดงโมดูลในเอกสาร HTML
ขั้นตอนสุดท้ายที่ต้องทำคือการส่งออกทั้งองค์ประกอบสคริปต์เดิมและสคริปต์สมัยใหม่ไปยังไฟล์ HTML ขออภัย ขณะนี้ปลั๊กอินที่สร้างไฟล์ HTML สุดท้าย HTMLWebpackPlugin
ยังไม่รองรับเอาต์พุตของสคริปต์ทั้งโมดูลและ nomodule แม้ว่าจะมีวิธีแก้ปัญหาชั่วคราวและปลั๊กอินแยกต่างหากที่สร้างขึ้นมาเพื่อแก้ปัญหานี้ เช่น BabelMultiTargetPlugin และ HTMLWebpackMultiBuildPlugin แต่เราจะใช้วิธีการที่ง่ายกว่าในการเพิ่มองค์ประกอบสคริปต์โมดูลด้วยตนเองเพื่อวัตถุประสงค์ของบทแนะนำนี้
เพิ่มข้อมูลต่อไปนี้ลงใน src/index.js
ที่ส่วนท้ายของไฟล์
...
</form>
<script type="module" src="main.mjs"></script>
</body>
</html>
จากนั้นโหลดแอปพลิเคชันในเบราว์เซอร์ที่รองรับโมดูล เช่น Chrome เวอร์ชันล่าสุด
ระบบจะดึงข้อมูลเฉพาะโมดูลที่มีขนาดแพ็กเกจเล็กกว่ามากเนื่องจากไม่มีการแปลงส่วนใหญ่ เบราว์เซอร์จะละเว้นองค์ประกอบสคริปต์อื่นๆ ทั้งหมด
หากคุณโหลดแอปพลิเคชันในเบราว์เซอร์รุ่นเก่า ระบบจะดึงข้อมูลเฉพาะสคริปต์ที่ผ่านการแปลงขนาดที่ใหญ่ขึ้นซึ่งมี polyfill และการเปลี่ยนรูปแบบที่จำเป็นทั้งหมด ต่อไปนี้คือภาพหน้าจอของคําขอทั้งหมดที่ส่งใน Chrome เวอร์ชันเก่า (เวอร์ชัน 38)
บทสรุป
ตอนนี้คุณเข้าใจวิธีใช้ @babel/preset-env
เพื่อระบุ polyfill ที่จําเป็นเท่านั้นสําหรับเบราว์เซอร์เป้าหมายแล้ว นอกจากนี้ คุณยังทราบด้วยว่าโมดูล JavaScript ช่วยปรับปรุงประสิทธิภาพให้ดียิ่งขึ้นได้อย่างไรด้วยการนำแอปพลิเคชันเวอร์ชันที่แปลงแล้ว 2 เวอร์ชันไปใช้งาน เมื่อเข้าใจดีแล้วว่าเทคนิคทั้ง 2 วิธีนี้ช่วยลดน้ำหนักไฟล์กลุ่มได้อย่างมาก ก็ลองไปเพิ่มประสิทธิภาพกันเลย