ลองทำ Monorepo ให้ JavaScript Project ด้วย Lerna
หลาย ๆ คนอาจเคยได้ยินคำว่า Monorepo กันมาบ้างแล้ว แล้วคุณรู้หรือไม่ว่า Monorepo มันคืออะไรกันแน่? Blog นี้เราจะมาอธิบายคำว่า Monorepo ให้คุณเข้าใจ และพาทำ Monorepo ให้ JavaScript Project โดยใช้ Lerna กัน
Monorepo คืออะไร?
เมื่อนานมาแล้วคุณอาจจะเคยเห็นหรืออาจเคยทำหลายๆ Project อยู่บน Git Repository เดียว (Monolith) ซึ่งเมื่อ Project ใหญ่ขึ้นมากๆ ก็จะพบกับความปวดหัวเพราะไฟล์เยอะแยะไปหมด แถมยังไม่มีจัดระเบียบไฟล์ให้เรียบร้อยอีกต่างหาก
ต่อมา Multirepo จึงเกิดขึ้น คือการแยกให้ 1 Git Repository มีแค่ 1 Project เท่านั้น แต่ปัญหาที่พบคือหาก Developer 1 คน อยู่หลาย Project ก็ต้องสลับ Repository ไปมาจนเหนื่อย จึงทำให้ทุกวันนี้มี “Monorepo” หรือ “Multi-package Repository” เกิดขึ้นมา ซึ่งมันคือการที่เราทำหลาย Project ใน 1 Git Repository นั่นเอง
ข้อดี-ข้อเสียของ Monorepo
ข้อดี
-
-
- เป็น Single Source of Truth
- จัดการกับ Dependency ได้ง่าย
- สามารถ Share/Reuse code ได้ง่าย
- สามารถแก้หลาย ๆ Project ได้ใน Commit เดียว
- สามารถเห็นภาพรวมของทุก Project ได้ง่าย
-
ข้อเสีย
-
-
- อาจจะต้องใช้ Tool ช่วยในการทำ
- มีจำนวนไฟล์ที่เยอะ
- หากมี Project ใดที่ Build Failed อาจทำให้ Project อื่นพังไปด้วย
-
มาเริ่มทำ Monorepo ด้วย Lerna กันเลย
Lerna เป็น Tool ที่ช่วยในการทำ Monorepo ให้กับ JavaScript Project
“ A tool for managing JavaScript projects with multiple packages. ”
ก่อนอื่นให้ทำการสร้าง Project เปล่า ๆ ขึ้นมาก่อน
mkdir my-monorepo
cd my-monorepo
จากนั้นให้ทำการเพิ่ม Lerna เข้าไปยัง Project
npx lerna init
yarn install
ซึ่งในขึ้นตอนนี้จะทำการสร้างไฟล์ต่าง ๆ ดังนี้
my-monorepo/ node_modules/ packages/ package.json lerna.json yarn.lock
ต่อมาเราจะทำการเพิ่ม Package แรกเข้าไปยัง Project และเพื่อให้เข้าใจได้ง่าย เราจะทำการสร้าง Package โดยใช้ Create React App ที่เรารู้จักกันดีนั้นเอง
cd packages
npx create-react-app react-package
cd ..
ต่อมาเราจะลองเพิ่ม Vue Package เข้าไปยัง Project บ้าง
cd packages
npx @vue/cli create --default vue-package
cd ..
เนื่องจากคำสั่งรันของ Vue จะใช้คำสั่ง serve แต่เพื่อให้ทุก Package ใช้คำสั่งตรงกันหมด จึงขอเข้าไปแก้ Script ใน package.json
// packages/vue-package/package.json{ "name": "vue-package", "version": "0.1.0", "private": true, "scripts": { "start": "vue-cli-service serve", // update this line from "serve" to "start" "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { "core-js": "^3.6.5", "vue": "^2.6.11" }, "devDependencies": { "@vue/cli-plugin-babel": "~4.4.0", "@vue/cli-plugin-eslint": "~4.4.0", "@vue/cli-service": "~4.4.0", "babel-eslint": "^10.1.0", "eslint": "^6.7.2", "eslint-plugin-vue": "^6.2.2", "vue-template-compiler": "^2.6.11" }, "eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/essential", "eslint:recommended" ], "parserOptions": { "parser": "babel-eslint" }, "rules": {} }, "browserslist": [ "> 1%", "last 2 versions", "not dead" ]}
ลองดูผลลัพธ์ตอนนี้ดูบ้าง
yarn lerna clean -y
yarn lerna bootstrap
yarn lerna run start
ผลลัพธ์จาก react-package http://localhost:3000
ผลลัพธ์จาก vue-package http://localhost:8080
เพียงเท่านี้เราก็สามารถมีหลาย Package ใน Project เดียวได้แล้ว แต่เดี๋ยวก่อน… ทุกคนคงสัยว่าทำแบบนี้แล้วจะต่างอะไรกับ Multirepo ล่ะ? งั้นเราลองสร้างอีก Package ที่จะเอาไปใช้ทั้งใน react-package กับ vue-package กัน
mkdir packages/shared-package
cd packages/shared-package
yarn init
cd ../..
จากนั้นทำการสร้างไฟล์ index.js และ .eslintrc.js เข้าไปให้ shared-package กัน
touch packages/shared-package/index.js
touch packages/shared-package/.eslintrc.js
// packages/shared-package/index.js
module.exports = 'Hello from shared-package'
// packages/shared-package/.eslintrc.js
module.exports = { "env": { "browser": true, "commonjs": true, "es2020": true }, "extends": "eslint:recommended", "parserOptions": { "ecmaVersion": 11 }, "rules": { }};
ทีนี้ลองทำการเรียกใช้ shared-package ใน react-package กัน
// packages/react-package/package.json{ "name": "react-package", "version": "0.1.0", "private": true, "dependencies": { "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-scripts": "3.4.1", "shared-package": "^1.0.0" // add this line }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }}
// packages/react-package/src/App.js
import sharedMessage from 'shared-package'; // add this line
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p>{sharedMessage}</p> {/* update this line */} <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> );}
export default App;
จากนั้นลองทำการเรียกใช้ shared-package ใน vue-package
// packages/vue-package/package.json{ "name": "vue-package", "version": "0.1.0", "private": true, "scripts": { "start": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { "core-js": "^3.6.5", "vue": "^2.6.11", "shared-package": "^1.0.0" // add this line }, "devDependencies": { "@vue/cli-plugin-babel": "~4.4.0", "@vue/cli-plugin-eslint": "~4.4.0", "@vue/cli-service": "~4.4.0", "babel-eslint": "^10.1.0", "eslint": "^6.7.2", "eslint-plugin-vue": "^6.2.2", "vue-template-compiler": "^2.6.11" }, "eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/essential", "eslint:recommended" ], "parserOptions": { "parser": "babel-eslint" }, "rules": {} }, "browserslist": [ "> 1%", "last 2 versions", "not dead" ]}
// packages/vue-package/src/App.vue<template> <div id="app"> <img alt="Vue logo" src="./assets/logo.png"> <HelloWorld :msg="message"/> <!-- update this line --> </div></template><script>
import sharedMessage from 'shared-package' // add this line
import HelloWorld from './components/HelloWorld.vue'
export default { name: 'App', components: { HelloWorld }, // add these codes computed: { message: function() { return sharedMessage } }}</script><style>#app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px;}</style>
มาดูผลลัพธ์ตอนนี้กันเลย
yarn lerna bootstrap
yarn lerna run start
เพียงเท่านี้เราก็ได้ข้อความ “Hello from shared-package” ที่ได้จาก shared-package มาใช้แล้ว
บทสรุป
Lerna เป็น tool ที่ช่วยให้เราสร้าง Monorepo ให้กับ JavaScript Project โดยการทำ Monorepo ก็มีทั้งข้อดี-ข้อเสีย การจะเลือกใช้หรือไม่ใช้ก็ควรพิจารณาความเหมาะสมของ Project ก่อน ซึ่งในบทความนี้เป็นเพียงการสอนการทำ Monorepo แบบง่าย ๆ เท่านั้น หากใครสนใจสามารถลองเอาไปพัฒนาต่อได้
แล้วอย่าลืมติดตามบทความอื่นๆ จากเราได้ที่ SennaLabs Blogs