SOFTWARE DEVELOPMENT | 5 mins read

ลองทำ Monorepo ให้ JavaScript Project ด้วย Lerna

By Pun on 05 Aug 2020
sennalabs-blog-banner

หลาย ๆ คนอาจเคยได้ยินคำว่า 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

 

ข้อดี

      1. เป็น Single Source of Truth
      2. จัดการกับ Dependency ได้ง่าย
      3. สามารถ Share/Reuse code ได้ง่าย
      4. สามารถแก้หลาย ๆ Project ได้ใน Commit เดียว
      5. สามารถเห็นภาพรวมของทุก Project ได้ง่าย

 

ข้อเสีย

      1. อาจจะต้องใช้ Tool ช่วยในการทำ
      2. มีจำนวนไฟล์ที่เยอะ
      3. หากมี 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

Written By
Pun