Nội dung bài viết
Với Create react app, bạn chỉ mất vài click để tạo 1 project React hoàn chỉnh, không yêu cầu kiến thức chuyên sâu gì về webpack hay babel. Nhưng nếu bạn vẫn chưa hài lòng với những tính năng mà CRA mang đến, ví dụ không hiện source-map CSS khi dev hay đơn giản là muốn cấu hình sâu hơn thì đã đến lúc bạn phải làm việc với Webpack rồi. Công ty mình toàn dùng webpack thôi chứ không có dùng Create react app
Nếu bạn chưa có kiến thức gì về webpack thì có thể tham khảo những bài này của mình trước nha:
- Webpack siêu tốc 1: Cấu hình Dev Server, Babel Loader, Style Loader, File Loader
- Webpack siêu tốc 2: Cấu hình Typescript, alias, hash bundle
Let’s go!
Cấu trúc thư mục
Đây là cấu trúc thư mục full, thư mục src là nơi chứa code React của chúng ta.
Khởi tạo Project
yarn init --yes hoặc npm init --yes
Cài đặt React
yarn add react react-dom
Cài đặt Webpack và các loader
yarn add webpack webpack-cli webpack-dev-server style-loader css-loader sass sass-loader file-loader typescript ts-loader -D
Giải thích
- webpack là phần lõi của webpack
- webpack-cli giúp ta gõ được lệnh của webpack trên terminal (gián tiếp thông qua file package.json)
- webpack-dev-server hỗ trợ tạo một server localhost cho môi trường dev
- style-loader, css-loader giúp bạn có thể import được css vào file js
- sass, sass-loader giúp bạn biên dịch scss sang css
- file-loader giúp bạn import được các file ví dụ như ảnh, video vào file js
- typescript: Phần lõi của ngôn ngữ Typescript
- ts-loader: Giúp tích hợp Typescript vào webpack
Cài đặt một số plugin bổ trợ webpack
yarn add clean-webpack-plugin compression-webpack-plugin copy-webpack-plugin dotenv-webpack html-webpack-plugin mini-css-extract-plugin webpack-bundle-analyzer -D
Giải thích
- clean-webpack-plugin: Giúp dọn dẹp thư mục build trước khi build webpack. Ví dụ thư mục build của bạn đang chứa bản build trước, bây giờ bạn build lại thì plugin này sẽ xóa bản build trước.
- compression-webpack-plugin: Giúp bạn nén các file css, js, html… thành gzip
- copy-webpack-plugin: Giúp bạn copy các file ở thư mục dev vào thư mục build. Ví dụ bạn có các file như favicon.ico, robots.txt cùng cấp với index.html, bạn muốn khi build xong thì các file này cũng có mặt ở bản build. Nếu không có plugin này thì bạn phải copy thủ công.
- dotenv-webpack: Giúp bạn dùng được các biến môi trường ở file .env và trong app của bạn
- html-webpack-plugin: Giúp clone ra 1 file index.html từ file html ban đầu. Tại sao lại cần clone thì bạn có thể tham khảo bài Webpack siêu tốc 2: Cấu hình Typescript, alias, hash bundle
- mini-css-extract-plugin: Bình thường thì css sẽ nằm trong file js sau khi build. Và khi chạy app thì js sẽ thêm các đoạn css đó vào thẻ
<style></style>
. Bây giờ mình không muốn như vậy, mình muốn css phải nằm ở file riêng biệt với js và khi chạy app thì js sẽ tự import bằng thẻ<link>
. Đó là chức năng của plugin này - webpack-bundle-analyzer: Giúp bạn phân tích bản build, coi thử thư viện nào đang chiếm bao nhiêu % bản build,…
Cài đặt ESLint và Prettier
yarn add eslint babel-eslint eslint-config-react-app eslint-loader eslint-plugin-flowtype eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser prettier eslint-plugin-prettier eslint-config-prettier -D
Giải thích: Ngoài prettier, eslint-config-prettier và eslint-plugin-prettier thì còn lại đều là các plugin tiêu chuẩn tương tự như bộ cài Create React App.
Thêm các file cấu hình
.eslintrc
{ "extends": ["react-app", "prettier"], "plugins": ["react", "prettier"], "rules": { "prettier/prettier": [ "warn", { "arrowParens": "avoid", "semi": false, "trailingComma": "none", "endOfLine": "lf", "tabWidth": 2, "printWidth": 80, "useTabs": false } ], "no-console": "warn" } }
.eslintignore
/src/serviceWorker.ts /src/setupTests.ts
.prettierrc
{ "arrowParens": "avoid", "semi": false, "trailingComma": "none", "endOfLine": "lf", "tabWidth": 2, "printWidth": 80, "useTabs": false }
.prettierignore
.cache package-lock.json
Thêm script vào package.json
Chèn đoạn mã dưới đây vào mục scripts trong file package.json
"start": "webpack serve --mode development", "build": "webpack --mode production", "build:analyze": "webpack --mode production --env analyze", "lint": "eslint --ext js,jsx,ts,tsx src/", "lint:fix": "eslint --fix --ext js,jsx,ts,tsx src/", "prettier": "prettier --check \"src/**/(*.tsx|*.ts|*.jsx|*.js|*.scss|*.css)\"", "prettier:fix": "prettier --write \"src/**/(*.tsx|*.ts|*.jsx|*.js|*.scss|*.css)\"",
Thêm file tsconfig.json để cấu hình Typescript
tsconfig.json
{ "compilerOptions": { "target": "ES5", "allowJs": true, "strict": true, "module": "ESNext", "moduleResolution": "node", "noImplicitAny": false, "sourceMap": true, "jsx": "react", "allowSyntheticDefaultImports": true, "baseUrl": ".", "paths": { "@/*": ["src/*"], "@@/*": ["./*"] } }, "include": ["src/**/*"] }
Đừng quên thêm public/index.html
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="logo192.png" /> <link rel="manifest" href="manifest.json" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> </body> </html>
Thêm file webpack.config.js
webpack.config.js
const path = require("path") const webpack = require("webpack") const HtmlWebpackPlugin = require("html-webpack-plugin") const CopyPlugin = require("copy-webpack-plugin") const Dotenv = require("dotenv-webpack") const MiniCssExtractPlugin = require("mini-css-extract-plugin") const { CleanWebpackPlugin } = require("clean-webpack-plugin") const CompressionPlugin = require("compression-webpack-plugin") const BundleAnalyzerPlugin = require("webpack-bundle-analyzer") .BundleAnalyzerPlugin const fs = require("fs") const directoryPath = path.resolve("public") const handleDir = () => { return new Promise((resolve, reject) => { fs.readdir(directoryPath, (err, files) => { if (err) { reject("Unable to scan directory: " + err) } resolve(files) }) }) } module.exports = async (env, agrv) => { const isDev = agrv.mode === "development" const isAnalyze = env && env.analyze const dirs = await handleDir() const copyPluginPatterns = dirs .filter(dir => dir !== "index.html") .map(dir => { return { from: dir, to: "", context: path.resolve("public") } }) const basePlugins = [ new Dotenv(), new HtmlWebpackPlugin({ template: "public/index.html" }), new CopyPlugin({ patterns: copyPluginPatterns }), new MiniCssExtractPlugin({ filename: isDev ? "[name].css" : "static/css/[name].[contenthash:6].css" }), new webpack.ProgressPlugin() ] let prodPlugins = [ ...basePlugins, new CleanWebpackPlugin(), new CompressionPlugin({ test: /\.(css|js|html|svg)$/ }) ] if (isAnalyze) { prodPlugins = [...prodPlugins, new BundleAnalyzerPlugin()] } return { entry: "./src/index.tsx", module: { rules: [ { test: /\.(ts|tsx)$/, use: ["ts-loader", "eslint-loader"], exclude: /node_modules/ }, { test: /\.(s[ac]ss|css)$/, use: [ MiniCssExtractPlugin.loader, { loader: "css-loader", options: { sourceMap: isDev ? true : false } }, { loader: "sass-loader", options: { sourceMap: isDev ? true : false } } ] }, { test: /\.(eot|ttf|woff|woff2)$/, use: [ { loader: "file-loader", options: { name: isDev ? "[path][name].[ext]" : "static/fonts/[name].[ext]" } } ] }, { test: /\.(png|svg|jpg|gif)$/, use: [ { loader: "file-loader", options: { name: isDev ? "[path][name].[ext]" : "static/media/[name].[contenthash:6].[ext]" } } ] } ] }, resolve: { extensions: [".tsx", ".ts", ".jsx", ".js"], alias: { "@": path.resolve("src"), "@@": path.resolve() } }, output: { path: path.resolve("build"), publicPath: "/", filename: "static/js/main.[contenthash:6].js", environment: { arrowFunction: false, bigIntLiteral: false, const: false, destructuring: false, dynamicImport: false, forOf: false, module: false } }, devtool: isDev ? "source-map" : false, devServer: { contentBase: "public", port: 3000, hot: true, watchContentBase: true, historyApiFallback: true, open: true }, plugins: isDev ? basePlugins : prodPlugins, performance: { maxEntrypointSize: 800000 // Khi có 1 file build vượt quá giới hạn này (tính bằng byte) thì sẽ bị warning trên terminal. } } }
Giải thích:
- isDev: Chúng ta có 2 mode là development và production tương đương với dev và build. 2 mode này được truyền vào thông qua –mode ở script trong package.json.
- isAnalyze: Nhìn vào file package.json chúng ta có câu lệnh build:analyze, mình có truyền biến analyze vào webpack thông qua –env. Biến này dùng để xác định bạn có dùng pluginBundleAnalyzerPlugin hay không.
- basePlugins: Những plugins dùng trong mode development.
Trong CopyPlugin ta thực hiện copy các file từ public sang thư mục build - CopyPlugin: Mình copy mọi file trong thư mục public vào thư mục build, ngoại trừ file index.html. Vì index.html đã có HtmlWebpackPlugin thực hiện việc copy và generate code, nếu không loại trừ sẽ bị xung đột!.
- webpack.ProgressPlugin() giúp chúng ta hiện % khi chạy webpack
- CompressionPlugin() giúp chúng ta nén file build thành gzip, thỉnh thoảng bạn sẽ thấy một số file kích thước nhỏ không được nén, bạn có thể xem cấu hình nén và điều kiện được nén tại đây
- prodPlugins: Những plugins dùng trong mode production.
- entry: File đầu vào cho webpack, file này thường là file import mọi file khác
- module.rules: Chứa các loader của webpack
- Các bạn để ý chỗ option.name ở file-loader: Đây là nơi bạn có thể thay đổi tên và đường dẫn file sau khi build. Môi trường dev thì mình giữ nguyên tên và đường dẫn (như vậy khi inspect trên trình duyệt sẽ dễ dàng thấy nguồn gốc file từ đâu ra), còn môi trường production thì mình sẽ chuyển vào thư mục static.
- contenthash:6 nghĩa là thêm 1 đoạn hash gồm 6 ký tự vào tên file.
- resolve.extensions: Thứ tự ưu tiên các file khi import
- alias: Tạo alias thuận tiện cho việc import trong webpack. Những nơi cần đường dẫn tuyệt đối thì ta phải dùng path.resolve() hoặc path.join()
- output.path: Đường dẫn thư mục build. Mình đặt tên thư mục build là build luôn, cho giống với Create React App
- output.filename: Tên file bundle sau khi được build. Cũng có thể quy định thư mục mà file build thuộc về
- output.publicPath: Chứa đường dẫn tương đối mà từ file index.html trỏ đến các file trong thư mục build sau khi build. Lưu ý là file index.html được build nằm trong thư mục tên là build. Ở các bài trước các bạn sẽ thấy mình để giá trị này là “”, nhưng bây giờ “/”. Lý do khi deploy thì “” trình duyệt nó bảo lỗi không load file được, nên phải thêm “/” vào nhé.
- output.environment: Mặc định webpack generate ra code dùng 1 số cú pháp của ES6, nhưng target mình mong muốn là ES5 nên mình cần chỉnh một số thông số như sau.
- arrowFunction: Hỗ trợ arrow function.
- bigIntLiteral: Hỗ trợ BigInt
- const: Hỗ trợ khai báo const và let
- destructuring: Hỗ trợ destructuring
- dynamicImport: Hỗ trợ async import
- forOf: Hỗ trợ vòng lặp forOf cho các array
- module: Hỗ trợ moudle ES6 (import … from ‘…’)’
- output.devtool: tùy chọn sourcemap
- devServer.contentBase: Chứa đường dẫn tương đối đến file
index.html
- devServer.port: port khi chạy localhost
- devServer.hot: Chế độ hot reload. Mặc định thì ở dev server thì webpack sẽ refresh lại trang mỗi khi có thay đổi nhỏ trong code.
- devServer.publicPath: Chứa đường dẫn tương đối từ thư mục root trỏ đến thư mục build (ở đây là dist). Chú ý phải thêm
/
ở trước và sau. Nhưng vì dùng HtmlWebpackPlugin nên ta sẽ tính từ chính thư mục dist. Vì thế giá trị cần dùng là/
. Ở đây mình không dùng giá trị nào cả, vì mặc định nó đã là/
- devServer.watchContentBase: Nếu bạn có thay đổi gì trong file index.html thì trình duyệt cũng tự động reload.
- devServer.historyApiFallback: Phải set true nếu không khi bạn dùng lazyload module React thì sẽ gặp lỗi không load được file.
- plugins: Chứa các plugin Webpack.
- performance.maxEntrypointSize: Khi có 1 file build vượt quá giới hạn này (tính bằng byte) thì sẽ bị warning trên terminal.
Lưu ý: anh em nhớ thêm folder @types để khai báo các file .d.ts phục vụ cho Typescript nhé, không Typescript báo lỗi khó chịu lắm đấy, chi tiết có thể coi Repository mình link bên dưới
Trên đây là giải thích cho một số cấu hình của webpack, còn lại mình nghĩ các bạn nhìn vào sẽ dễ dàng hiểu, nếu không hiểu đoạn nào thì có thể comment phía dưới, mình sẽ giải đáp sớm nhất. Hoặc coi lại Bài 1 và Bài 2 của mình
Để chạy khi dev
yarn start
Để build ra thành phẩm phục vụ deploy
yarn build
Để build và phân tích source code
yarn build:analyze
Ngoài ra bạn có thể yarn lint
, yarn lint:fix
, yarn prettier
, yarn prettier:fix
như đã định nghĩa trong file package.json
Thư mục build
Tóm lại
Các bạn học về webpack thì cũng nên đọc doc của các plugin mà mình dùng trong bài để coi schema nó dùng như thế nào, vì đôi khi các bạn nhìn vào mình dùng mà không hiểu vì sao lại truyền tham số như vậy. Thực hành đi thực hành lại cho nhiều mới thuần thục được nha.
Cảm ơn mọi người đã đọc đến đây, bài này khá dài và yêu cầu kiến thức khá nhiều, hy vọng mình đã giải thích chi tiết để mọi người có thể hiểu được. Hẹn gặp lại mọi người ở các bài tiếp theo
Github: link github project, folder bài 3 nhé
Ad ơi cho mình hỏi mấy vấn đề sau:
1. Cấu trúc này mình muốn phân tách ra thành nhiều module, mỗi module quản lý một store riêng thì setting như thế nào ad.
2. Trong package.json devDependencies mình thấy có sử các thư viện @types/… dùng để làm gì vậy ad, thấy ad không mô tả trong bài viết.
3. Do khi start project từ git của ad thì không tự bật trình duyệt, nên mình dùng lệnh webpack-dev-server –mode development –open –hot để start project đang báo lỗi thiếu MODULE_NOT_FOUND, có cần cài thêm gì nữa không ad?
Mình trả lời câu hỏi bạn như sau
1. Hiện tại cấu trúc mình đang chia theo module, mỗi module đang quản lý 1 store riêng đó bạn
2. Khi dùng Typescript thì bạn phải thêm các devDependencies là @types/… vào để TS có thể nhận biết type của thư viện đó nhé. Cái này về Typescript rồi nên mình k mô tả.
3. Từ webpack 5 trở đi thì bạn không còn dùng lệnh webpack-dev-server nữa mà chuyển thành webpack thôi, vậy nên nó mới báo lỗi module_not_found đó. Còn muốn tự động mở trình duyệt thì bạn thêm open: true vào mục devServer trong webpack là được nhé.
mình ko thấy bạn dùng babel để dùng đk reactjs hay convert code sang js nhỉ
Không cần bạn, vì TS Loader làm việc này giúp mình rồi
Ad ơi nếu được mong ad ra mắt thêm 1 số thứ ạ:
1 – Webpack: mong ad bổ sung vào bài này 1 số plugin để giảm kích thước các file build
2 – Bài viết chi tiết về cấu trúc folder của reactjs ạ!
3 – Ngoài ra có 1 số chi tiết nhỏ như eslint hoặc prettier.
4 – Tại sao asset lại nằm trong src? và cho em hỏi làm sao để gọi image từ component đến nơi chứa image như public hoặc asset?
Mình cảm ơn ad nhiều
Nếu được mong ad ra mắt thêm 1 số thứ ạ:
1 – Mong ad bổ sung vào 1 số plugin để giảm kích thước các file build (Nếu được mong ad chỉ đoạn object setting)
2 – Bài viết chi tiết về cấu trúc folder của reactjs!
3 – Ngoài ra có 1 số chi tiết nhỏ như eslint hoặc prettier, .env.
4 – Tại sao assets lại nằm trong src? Làm sao để gọi image từ component đến nơi chứa image như public hoặc assets?
5 – Folder static trong build có phải là vendor mà đa phần hay để phải không ạ?
6 – Setting là file .ts nhưng nếu để .js thì sẽ không bị ảnh hưởng gì và vẫn build như thường phải không?
7 – Trong phần này nếu có thì config babel vẫn được nhỉ? trong trường hợp không config ts ý ạ.
Cảm ơn ad rất nhiều
1. Chính là CompressionPlugin đó bạn, trong bài mình có dùng mà
2. Chi tiết về cấu trúc folder ReactJs thì tùy mỗi người, bạn có thể tham khảo trong link repo cuối bài hoặc đọc bài giải thích này: https://xdevclass.com/cau-truc-react-folder-toi-uu-de-bao-tri-de-nang-cap/
3. Đọc tại đây: https://xdevclass.com/cai-dat-moi-truong-code-react-toi-uu-vs-code-prettier-eslint/
4. Assets nằm trong src cho giống cấu trúc của Create React App cũng như cấu trúc phổ biến của các framework, đó không phải là rule nhưng là quy ước thôi, cho dễ nhìn và bảo trì. Các thứ nằm trong public là để dùng trong file index.html hoặc bổ trợ như robot.txt
5. Static chỉ là nơi chứa các file css, js, font, media thôi. Bạn có thể build ra để xem
6. đúng
7. Vẫn được
Cho mình hỏi, sau khi chạy yarn build ra được folder build rồi. Lúc đó mở file index.html trong folder build lên thì lại không thấy gì. Có cần phải thao tác gì nữa không ạ?