คู่มือการจัดการการพึ่งพา Webpack

เผยแพร่แล้ว: 2022-03-11

แนวคิดของการทำให้เป็นโมดูลเป็นส่วนหนึ่งของภาษาโปรแกรมที่ทันสมัยที่สุด อย่างไรก็ตาม JavaScript ยังขาดแนวทางที่เป็นทางการในการทำให้เป็นโมดูล จนกระทั่ง ECMAScript ES6 เวอร์ชันล่าสุดมาถึง

ใน Node.js หนึ่งในเฟรมเวิร์ก JavaScript ที่ได้รับความนิยมมากที่สุดในปัจจุบัน ตัวรวมโมดูลอนุญาตให้โหลดโมดูล NPM ในเว็บเบราว์เซอร์ และไลบรารีที่เน้นองค์ประกอบ (เช่น React) สนับสนุนและอำนวยความสะดวกในการปรับโค้ด JavaScript ให้เป็นโมดูล

Webpack เป็นหนึ่งในชุดรวมโมดูลที่พร้อมใช้งานซึ่งประมวลผลโค้ด JavaScript รวมถึงเนื้อหาสแตติกทั้งหมด เช่น สไตล์ชีต รูปภาพ และฟอนต์ ลงในไฟล์ที่รวมกลุ่ม การประมวลผลสามารถรวมงานที่จำเป็นทั้งหมดสำหรับการจัดการและเพิ่มประสิทธิภาพการพึ่งพาโค้ด เช่น การคอมไพล์ การต่อข้อมูล การลดขนาด และการบีบอัด

Webpack: บทช่วยสอนสำหรับผู้เริ่มต้น

อย่างไรก็ตาม การกำหนดค่า Webpack และการขึ้นต่อกันอาจทำให้เครียดและไม่ใช่กระบวนการที่ตรงไปตรงมาเสมอไป โดยเฉพาะสำหรับผู้เริ่มต้น

โพสต์บล็อกนี้ให้แนวทาง พร้อมตัวอย่าง วิธีกำหนดค่า Webpack สำหรับสถานการณ์ต่างๆ และชี้ให้เห็นข้อผิดพลาดที่พบบ่อยที่สุดที่เกี่ยวข้องกับการรวมกลุ่มของการพึ่งพาโครงการโดยใช้ Webpack

ส่วนแรกของโพสต์ในบล็อกนี้จะอธิบายวิธีทำให้คำจำกัดความของการพึ่งพาในโครงการง่ายขึ้น ต่อไป เราจะหารือและสาธิตการกำหนดค่าสำหรับการแยกโค้ดของแอปพลิเคชันแบบหลายหน้าและแบบหน้าเดียว สุดท้ายนี้ เราจะพูดถึงวิธีกำหนดค่า Webpack หากเราต้องการรวมไลบรารีของบุคคลที่สามในโครงการของเรา

การกำหนดค่านามแฝงและเส้นทางสัมพัทธ์

เส้นทางสัมพัทธ์ไม่เกี่ยวข้องโดยตรงกับการขึ้นต่อกัน แต่เราใช้มันเมื่อเรากำหนดการอ้างอิง หากโครงสร้างไฟล์ของโปรเจ็กต์มีความซับซ้อน อาจเป็นเรื่องยากที่จะแก้ไขพาธของโมดูลที่เกี่ยวข้อง ประโยชน์พื้นฐานที่สุดประการหนึ่งของการกำหนดค่า Webpack คือช่วยให้คำจำกัดความของพาธสัมพัทธ์ในโปรเจ็กต์ง่ายขึ้น

สมมติว่าเรามีโครงสร้างโครงการดังต่อไปนี้:

 - Project - node_modules - bower_modules - src - script - components - Modal.js - Navigation.js - containers - Home.js - Admin.js

เราสามารถอ้างอิงการพึ่งพาโดยพาธสัมพัทธ์ไปยังไฟล์ที่เราต้องการ และหากเราต้องการนำเข้าส่วนประกอบไปยังคอนเทนเนอร์ในซอร์สโค้ดของเรา จะมีลักษณะดังนี้:


Home.js

 Import Modal from '../components/Modal'; Import Navigation from '../components/Navigation';


Modal.js

 import {datepicker} from '../../../../bower_modules/datepicker/dist/js/datepicker';

ทุกครั้งที่เราต้องการนำเข้าสคริปต์หรือโมดูล เราจำเป็นต้องทราบตำแหน่งของไดเร็กทอรีปัจจุบันและค้นหาเส้นทางที่สัมพันธ์กับสิ่งที่เราต้องการนำเข้า เราสามารถจินตนาการได้ว่าปัญหานี้จะบานปลายในความซับซ้อนได้อย่างไรถ้าเรามีโครงการขนาดใหญ่ที่มีโครงสร้างไฟล์ที่ซ้อนกัน หรือเราต้องการปรับโครงสร้างบางส่วนของโครงสร้างโครงการที่ซับซ้อน

เราสามารถจัดการกับปัญหานี้ได้อย่างง่ายดายด้วยตัวเลือก fix.alias ของ resolve.alias เราสามารถประกาศชื่อแทน - ชื่อของไดเร็กทอรีหรือโมดูลพร้อมตำแหน่ง และเราไม่พึ่งพาพาธสัมพัทธ์ในซอร์สโค้ดของโปรเจ็กต์


webpack.config.js

 resolve: { alias: { 'node_modules': path.join(__dirname, 'node_modules'), 'bower_modules': path.join(__dirname, 'bower_modules'), } }

ในไฟล์ Modal.js เราสามารถนำเข้า datepicker ได้ง่ายกว่ามาก:

 import {datepicker} from 'bower_modules/datepicker/dist/js/datepicker';

การแยกรหัส

เราสามารถมีสถานการณ์ที่เราจำเป็นต้องต่อท้ายสคริปต์ลงในบันเดิลสุดท้าย หรือแยกบันเดิลสุดท้าย หรือเราต้องการโหลดบันเดิลแยกกันตามความต้องการ การตั้งค่าโปรเจ็กต์และการกำหนดค่า Webpack สำหรับสถานการณ์เหล่านี้อาจไม่ตรงไปตรงมา

ในการกำหนดค่า Webpack ตัวเลือก Entry จะบอก Webpack ว่าจุดเริ่มต้นอยู่ที่ใดสำหรับบันเดิลสุดท้าย จุดเริ่มต้นสามารถมีประเภทข้อมูลที่แตกต่างกันสามประเภท: สตริง อาร์เรย์ หรืออ็อบเจ็กต์

หากเรามีจุดเริ่มต้นเพียงจุดเดียว เราสามารถใช้รูปแบบใดก็ได้เหล่านี้และได้ผลลัพธ์แบบเดียวกัน

หากเราต้องการผนวกไฟล์หลาย ๆ ไฟล์ และไม่ขึ้นอยู่กับแต่ละไฟล์ เราสามารถใช้รูปแบบ Array ได้ ตัวอย่างเช่น เราสามารถผนวก analytics.js ต่อท้าย bundle.js :


webpack.config.js

 module.exports = { // creates a bundle out of index.js and then append analytics.js entry: ['./src/script/index.jsx', './src/script/analytics.js'], output: { path: './build', filename: bundle.js ' } };

การจัดการจุดเข้าใช้งานหลายจุด

สมมติว่าเรามีแอปพลิเคชันแบบหลายหน้าที่มีไฟล์ HTML หลายไฟล์ เช่น index.html และ admin.html เราสามารถสร้างบันเดิลได้หลายชุดโดยใช้จุดเริ่มต้นเป็นประเภทอ็อบเจ็กต์ การกำหนดค่าด้านล่างสร้างบันเดิล JavaScript สองชุด:


webpack.config.js

 module.exports = { entry: { index: './src/script/index.jsx', admin: './src/script/admin.jsx' }, output: { path: './build', filename: '[name].js' // template based on keys in entry above (index.js & admin.js) } };


index.html

 <script src=”build/index.js”></script>


admin.html

 <script src=”build/admin.js”></script>

บันเดิล JavaScript ทั้งสองสามารถแชร์ไลบรารีและส่วนประกอบทั่วไปได้ เพื่อสิ่งนี้ เราสามารถใช้ CommonsChunkPlugin ซึ่งค้นหาโมดูลที่เกิดขึ้นในหลาย ๆ รายการและสร้างบันเดิลที่ใช้ร่วมกันที่สามารถแคชระหว่างหลาย ๆ เพจได้


webpack.config.js

 var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js'); module.exports = { entry: { index: './src/script/index.jsx', admin: './src/script/admin.jsx' }, output: { path: './build', filename: '[name].js' // template based on keys in entry above (index.js & admin.js) }, plugins: [commonsPlugin] };

ตอนนี้ เราต้องไม่ลืมที่จะเพิ่ม <script src="build/common.js"></script> ก่อนการรวมสคริปต์

เปิดใช้งาน Lazy Loading

Webpack สามารถแบ่งทรัพย์สินแบบคงที่ออกเป็นชิ้นเล็กๆ ได้ และวิธีนี้มีความยืดหยุ่นมากกว่าการต่อข้อมูลแบบมาตรฐาน หากเรามีแอปพลิเคชันหน้าเดียวขนาดใหญ่ (SPA) การต่อกันอย่างง่ายเป็นชุดเดียวไม่ใช่วิธีที่ดีเพราะการโหลดชุดรวมขนาดใหญ่หนึ่งชุดอาจทำได้ช้า และผู้ใช้มักจะไม่ต้องการการพึ่งพาทั้งหมดในแต่ละมุมมอง

เราได้อธิบายก่อนหน้านี้ถึงวิธีแบ่งแอปพลิเคชันออกเป็นหลายบันเดิล เชื่อมโยงการพึ่งพาทั่วไป และได้รับประโยชน์จากพฤติกรรมการแคชของเบราว์เซอร์ วิธีนี้ใช้ได้ผลดีกับแอปพลิเคชันแบบหลายหน้า แต่ไม่ใช่สำหรับแอปพลิเคชันแบบหน้าเดียว

สำหรับ SPA เราควรจัดเตรียมเฉพาะเนื้อหาคงที่ที่จำเป็นสำหรับการแสดงผลมุมมองปัจจุบัน เราเตอร์ฝั่งไคลเอ็นต์ในสถาปัตยกรรม SPA เป็นสถานที่ที่สมบูรณ์แบบในการจัดการการแยกโค้ด เมื่อผู้ใช้เข้าสู่เส้นทาง เราสามารถโหลดเฉพาะการขึ้นต่อกันที่จำเป็นสำหรับมุมมองผลลัพธ์ อีกทางหนึ่ง เราสามารถโหลดการขึ้นต่อกันเมื่อผู้ใช้เลื่อนหน้าลง

เพื่อจุดประสงค์นี้ เราสามารถใช้ฟังก์ชัน require.ensure หรือ System.import ซึ่ง Webpack สามารถตรวจจับแบบสแตติกได้ Webpack สามารถสร้างบันเดิลแยกต่างหากตามจุดแยกนี้และเรียกได้ตามต้องการ

ในตัวอย่างนี้ เรามีคอนเทนเนอร์ React สองคอนเทนเนอร์ มุมมองผู้ดูแลระบบและมุมมองแดชบอร์ด


admin.jsx

 import React, {Component} from 'react'; export default class Admin extends Component { render() { return <div > Admin < /div>; } }


dashboard.jsx

 import React, {Component} from 'react'; export default class Dashboard extends Component { render() { return <div > Dashboard < /div>; } }

หากผู้ใช้ป้อน URL /dashboard หรือ /admin จะโหลดเฉพาะบันเดิล JavaScript ที่จำเป็นที่เกี่ยวข้องเท่านั้น ด้านล่างนี้ เราสามารถดูตัวอย่างที่มีและไม่มีเราเตอร์ฝั่งไคลเอ็นต์


index.jsx

 if (window.location.pathname === '/dashboard') { require.ensure([], function() { require('./containers/dashboard').default; }); } else if (window.location.pathname === '/admin') { require.ensure([], function() { require('./containers/admin').default; }); }


index.jsx

 ReactDOM.render( <Router> <Route path="/" component={props => <div>{props.children}</div>}> <IndexRoute component={Home} /> <Route path="dashboard" getComponent={(nextState, cb) => { require.ensure([], function (require) { cb(null, require('./containers/dashboard').default) }, "dashboard")}} /> <Route path="admin" getComponent={(nextState, cb) => { require.ensure([], function (require) { cb(null, require('./containers/admin').default) }, "admin")}} /> </Route> </Router> , document.getElementById('content') );

การแยกสไตล์ออกเป็นบันเดิลที่แยกจากกัน

ใน Webpack ตัวโหลด เช่น style-loader และ css-loader ประมวลผลสไตล์ชีตล่วงหน้าและฝังลงในบันเดิล JavaScript เอาต์พุต แต่ในบางกรณี สิ่งเหล่านี้อาจทำให้ Flash ของเนื้อหาที่ไม่มี สไตล์ (FOUC)

เราสามารถหลีกเลี่ยง FOUC ด้วย ExtractTextWebpackPlugin ที่อนุญาตให้สร้างสไตล์ทั้งหมดเป็นบันเดิล CSS ที่แยกจากกัน แทนที่จะฝังไว้ในบันเดิล JavaScript สุดท้าย


webpack.config.js

 var ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { module: { loaders: [{ test: /\.css/, loader: ExtractTextPlugin.extract('style', 'css')' }], }, plugins: [ // output extracted CSS to a file new ExtractTextPlugin('[name].[chunkhash].css') ] }

การจัดการไลบรารีและปลั๊กอินของบุคคลที่สาม

หลายครั้งที่เราจำเป็นต้องใช้ไลบรารีของบุคคลที่สาม ปลั๊กอินต่างๆ หรือสคริปต์เพิ่มเติม เนื่องจากเราไม่ต้องการใช้เวลาในการพัฒนาส่วนประกอบเดียวกันตั้งแต่เริ่มต้น มีไลบรารีและปลั๊กอินรุ่นเก่าจำนวนมากที่ไม่ได้รับการดูแล ไม่เข้าใจโมดูล JavaScript และถือว่ามีการอ้างอิงทั่วโลกภายใต้ชื่อที่กำหนดไว้ล่วงหน้า

ด้านล่างนี้คือตัวอย่างบางส่วนของปลั๊กอิน jQuery พร้อมคำอธิบายเกี่ยวกับวิธีกำหนดค่า Webpack อย่างถูกต้องเพื่อให้สามารถสร้างบันเดิลสุดท้ายได้

จัดเตรียมปลั๊กอิน

ปลั๊กอินบุคคลที่สามส่วนใหญ่อาศัยการมีอยู่ของการพึ่งพาทั่วโลกที่เฉพาะเจาะจง ในกรณีของ jQuery ปลั๊กอินจะขึ้นอยู่กับตัวแปร $ หรือ jQuery ที่กำหนดไว้ และเราสามารถใช้ปลั๊กอิน jQuery ได้โดยการเรียก $('div.content').pluginFunc() ในโค้ดของเรา

เราสามารถใช้ปลั๊กอิน ProvidePlugin เพื่อเติม var $ = require("jquery") ทุกครั้งที่พบ global $ identifier


webpack.config.js

 webpack.ProvidePlugin({ '$': 'jquery', })

เมื่อ Webpack ประมวลผลโค้ด โปรแกรมจะค้นหาสถานะ $ และให้การอ้างอิงถึงการพึ่งพาส่วนกลางโดยไม่ต้องนำเข้าโมดูลที่ระบุโดยฟังก์ชัน require

นำเข้า-loader

ปลั๊กอิน jQuery บางตัวถือว่า $ ในเนมสเปซส่วนกลางหรืออาศัย this เป็นวัตถุ window เพื่อจุดประสงค์นี้ เราสามารถใช้ imports-loader ซึ่งแทรกตัวแปรส่วนกลางลงในโมดูล


example.js

 $('div.content').pluginFunc();

จากนั้น เราสามารถแทรกตัวแปร $ ลงในโมดูลโดยกำหนดค่า imports-loader :

 require("imports?$=jquery!./example.js");

นี่เป็นเพียงการ var $ = require("jquery"); ไปที่ example.js

ในกรณีการใช้งานที่สอง:


webpack.config.js

 module: { loaders: [{ test: /jquery-plugin/, loader: 'imports?jQuery=jquery,$=jquery,this=>window' }] }

โดยใช้สัญลักษณ์ => (เพื่อไม่ให้สับสนกับฟังก์ชัน ES6 Arrow) เราสามารถตั้งค่าตัวแปรตามอำเภอใจได้ ค่าสุดท้ายกำหนดตัวแปรส่วนกลางใหม่เพื่อชี้ไป this วัตถุ window เหมือนกับการตัดเนื้อหาทั้งหมดของไฟล์ด้วย (function () { ... }).call(window); และเรียกใช้ฟังก์ชัน this ด้วย window เป็นอาร์กิวเมนต์

เรายังต้องการไลบรารี่โดยใช้รูปแบบโมดูล CommonJS หรือ AMD:

 // CommonJS var $ = require("jquery"); // jquery is available // AMD define(['jquery'], function($) { // jquery is available });

ไลบรารีและโมดูลบางตัวสามารถรองรับรูปแบบโมดูลต่างๆ ได้

ในตัวอย่างต่อไป เรามีปลั๊กอิน jQuery ซึ่งใช้รูปแบบโมดูล AMD และ CommonJS และมีการพึ่งพา jQuery:


jquery-plugin.js

 (function(factory) { if (typeof define === 'function' && define.amd) { // AMD format is used define(['jquery'], factory); } else if (typeof exports === 'object') { // CommonJS format is used module.exports = factory(require('jquery')); } else { // Neither AMD nor CommonJS used. Use global variables. } });


webpack.config.js

 module: { loaders: [{ test: /jquery-plugin/, loader: "imports?define=>false,exports=>false" }] }

เราสามารถเลือกรูปแบบโมดูลที่เราต้องการใช้สำหรับไลบรารีเฉพาะ หากเราประกาศ define false เท่ากัน Webpack จะไม่แยกวิเคราะห์โมดูลในรูปแบบโมดูล AMD และหากเราประกาศการ exports ตัวแปร false เท่ากับ Webpack จะไม่แยกวิเคราะห์โมดูลในรูปแบบโมดูล CommonJS

เปิดเผยโหลด

หากเราจำเป็นต้องเปิดเผยโมดูลต่อบริบทส่วนกลาง เราก็สามารถใช้ expose-loader ได้ สิ่งนี้มีประโยชน์ ตัวอย่างเช่น หากเรามีสคริปต์ภายนอกที่ไม่ได้เป็นส่วนหนึ่งของการกำหนดค่า Webpack และอาศัยสัญลักษณ์ในเนมสเปซส่วนกลาง หรือเราใช้ปลั๊กอินของเบราว์เซอร์ที่จำเป็นต้องเข้าถึงสัญลักษณ์ในคอนโซลของเบราว์เซอร์


webpack.config.js

 module: { loaders: [ test: require.resolve('jquery'), loader: 'expose-loader?jQuery!expose-loader?$' ] }

ขณะนี้ไลบรารี jQuery พร้อมใช้งานในเนมสเปซส่วนกลางสำหรับสคริปต์อื่นๆ บนหน้าเว็บ

 window.$ window.jQuery

การกำหนดค่าการพึ่งพาภายนอก

หากเราต้องการรวมโมดูลจากสคริปต์ที่โฮสต์ภายนอก เราจำเป็นต้องกำหนดโมดูลเหล่านี้ในการกำหนดค่า มิฉะนั้น Webpack จะไม่สามารถสร้างบันเดิลสุดท้ายได้

เราสามารถกำหนดค่าสคริปต์ภายนอกได้โดยใช้ตัวเลือก externals ในการกำหนดค่า Webpack ตัวอย่างเช่น เราสามารถใช้ไลบรารี่จาก CDN ผ่านแท็ก <script> แยกต่างหาก ในขณะที่ยังคงประกาศอย่างชัดเจนว่าเป็นการพึ่งพาโมดูลในโครงการของเรา


webpack.config.js

 externals: { react: 'React', 'react-dom': 'ReactDOM' }

รองรับหลายอินสแตนซ์ของไลบรารี

เป็นการดีที่จะใช้ตัวจัดการแพ็คเกจ NPM ในการพัฒนาส่วนหน้าสำหรับจัดการไลบรารีและการพึ่งพาของบุคคลที่สาม อย่างไรก็ตาม บางครั้งเราสามารถมีไลบรารีเดียวกันได้หลายอินสแตนซ์โดยใช้เวอร์ชันต่างๆ กัน และไม่สามารถทำงานร่วมกันได้ดีในสภาพแวดล้อมเดียว

สิ่งนี้อาจเกิดขึ้นได้ ตัวอย่างเช่น กับไลบรารี React ซึ่งเราสามารถติดตั้ง React จาก NPM และหลังจากนั้น React เวอร์ชันอื่นจะสามารถใช้งานได้กับแพ็คเกจหรือปลั๊กอินเพิ่มเติม โครงสร้างโครงการของเราสามารถมีลักษณะดังนี้:

 project | |-- node_modules | |-- react |-- react-plugin | |--node_modules | |--react

ส่วนประกอบที่มาจาก react-plugin มีอินสแตนซ์ React ที่แตกต่างจากส่วนประกอบที่เหลือในโปรเจ็กต์ ตอนนี้เรามี React สองชุดแยกกัน และพวกมันสามารถเป็นเวอร์ชันที่แตกต่างกันได้ ในแอปพลิเคชันของเรา สถานการณ์นี้อาจทำให้ DOM ที่ไม่แน่นอนทั่วโลกของเรายุ่งเหยิง และเราสามารถเห็นข้อความแสดงข้อผิดพลาดในบันทึกของเว็บคอนโซล วิธีแก้ปัญหานี้คือต้องมี React เวอร์ชันเดียวกันตลอดทั้งโปรเจ็กต์ เราสามารถแก้ไขได้ด้วยนามแฝง Webpack


webpack.config.js

 module.exports = { resolve: { alias: { 'react': path.join(__dirname, './node_modules/react'), 'react/addons': path.join(__dirname, '/node_modules/react/addons'), } } }

เมื่อ react-plugin พยายามต้องการ React มันจะใช้เวอร์ชันใน node_modules ของโปรเจ็กต์ หากเราต้องการทราบว่าเราใช้ React เวอร์ชันใด เราสามารถเพิ่ม console.log(React.version) ลงในซอร์สโค้ดได้

เน้นที่การพัฒนา ไม่ใช่การกำหนดค่า Webpack

โพสต์นี้เป็นเพียงรอยขีดข่วนพื้นผิวของพลังและประโยชน์ของ Webpack

มีตัวโหลดและปลั๊กอิน Webpack อื่นๆ อีกมากมายที่จะช่วยคุณเพิ่มประสิทธิภาพและปรับปรุงการรวมกลุ่ม JavaScript

แม้ว่าคุณจะเป็นมือใหม่ คู่มือนี้จะช่วยให้คุณมีพื้นฐานที่ดีในการเริ่มใช้ Webpack ซึ่งจะช่วยให้คุณมุ่งเน้นที่การพัฒนามากขึ้นน้อยลงในการกำหนดค่าการรวมกลุ่ม

ที่เกี่ยวข้อง: รักษาการควบคุม: คู่มือ Webpack และ React, Pt. 1