Nguồn: Intercom
2. Phương pháp thực hiện và những lưu ý
Việc thực hiện xây dựng một Widget chỉ đơn giản là cung cấp các đoạn mã để chèn thêm DOM element nhằm thực hiện các tính năng mong muốn vào “host” website.
Tuy nhiên, việc chèn thêm các element mới vào website có một số điểm phải lưu ý:
- Xung đột style, script với trang web gốc
- Bundle size của script
- Các feature mà widget sử dụng không được hỗ trợ trên trình duyệt đang chạy website.
Với các lưu ý này, chúng ta có thể có nhiều cách để có thể giải quyết. Với các công nghệ xây dựng web ngày nay thì chúng ta hiện tại đều đã có sẵn các webpack plugin, polyfill tương ứng để giúp chúng ta dễ dàng giải quyết các lưu ý khi thực hiện một javascript widget.
3. Thực hành xây dựng embedded script với các giải pháp web application quen thuộc
Trong bài viết lần này, chúng ta sẽ tìm hiểu việc xây dựng 1 embedded script với 2 giải pháp trending hiện nay là React và Angular. Với React chúng ta sẽ sử dụng webpack và webpack-cli, còn với Angular chúng ta sẽ sử dụng giải pháp Angular Element để thực hiện.
React
Với cách triển khai này, ta sẽ thực hiện như 1 React app bình thường, tuy nhiên sẽ cần 1 số cấu hình webpack custom để có thể có đc “thành quả” như ý muốn.
Khác với React app thông thường sẽ sử dụng file app.tsx để có thể mount lên DOM thuộc index.html, thì ở đây chúng ta sẽ thực hiện entry khác với thông thường.
File: embeddable-widget.tsx
export default class WidgetBooking {
static htmlelment: any;
static mount(
id: string,
locale: "en" | "vi" = "en",
fullscreened: boolean = false,
divId: string = ""
) {
const component = (
<Widget id={id} locale={locale} fullscreened={fullscreened} divId={divId} />
);
function doRender() {
if (WidgetBooking.htmlelment) {
throw new Error("EmbeddableWidget is already mounted, unmount first");
}
let htmlelment = null;
if (divId) {
htmlelment = document.querySelector(`#${divId}`);
} else {
htmlelment = document.createElement("div");
htmlelment.style.position = "fixed";
htmlelment.style.bottom = "20px";
htmlelment.style.right = "20px";
document.body.appendChild(htmlelment);
}
ReactDOM.render(component, htmlelment);
WidgetBooking.htmlelment = htmlelment;
}
if (document.readyState === "complete") {
doRender();
} else {
window.addEventListener("load", () => {
doRender();
});
}
}
static unmount() {
if (!WidgetBooking.el) {
throw new Error("EmbeddableWidget is not mounted, mount first");
}
ReactDOM.unmountComponentAtNode(WidgetBooking.htmlelment);
WidgetBooking.htmlelment.parentNode.removeChild(WidgetBooking.htmlelment);
WidgetBooking.htmlelment = null;
}
}
Với webpack ta sẽ có 3 files:
Webpack.config.base.js
const CleanWebpackPlugin = require("clean-webpack-plugin");
const path = require("path");
module.exports = {
entry: {
widget: "./src/outputs/embeddable-widget.tsx",
},
output: {
path: path.resolve(__dirname, "dist"),
publicPath: "/",
filename: "[name].js",
library: "WidgetBooking",
libraryExport: "default",
libraryTarget: "window"
},
plugins: [new CleanWebpackPlugin()],
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
plugins: []
}
},
"ts-loader"
]
},
{
test: /\.(scss|css)$/,
use: [
"style-loader",
"css-loader",
"sass-loader",
"cssimportant-loader"
]
}
]
},
resolve: {
extensions: ["*", ".js", ".jsx", ".ts", ".tsx"]
},
performance: {
maxEntrypointSize: 2048000,
maxAssetSize: 2048000
}
};
Webpack.config.dev.js
const base = require("./webpack.config.base");
const merge = require("webpack-merge");
module.exports = merge(base, {
mode: "development"
});
Webpack.config.prod.js
const base = require("./webpack.config.base");
const merge = require("webpack-merge");
const webpack = require("webpack");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const JavaScriptObfuscator = require("webpack-obfuscator");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
.BundleAnalyzerPlugin;
module.exports = merge(base, {
mode: "production",
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: "static"
}),
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: "[name].[hash].css",
chunkFilename: "[id].[hash].css"
}),
new JavaScriptObfuscator(),
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /vi/)
]
});
Khi đã đầy đủ các cấu hình như vậy, chúng ta sẽ provide các script cần thiết ở trong package.json để có thể bắt đầu development cũng như build embedded script.
"scripts": {
"build": "webpack-cli --config ./webpack.config.prod.js",
"start": "webpack-serve --config ./webpack.config.dev.js --open",
"test": "jest",
"test-update-snapshots": "jest --updateSnapshot"
},
Sau khi thành phẩm được phát triển và sẵn sàng được sử dụng, việc sử dụng script tại các website trở nên rất dễ dàng.
<script src="./widget.js"></script>
<script>
WidgetBooking.mount(
'idXXX',
"en",
true // fullscreen
);
</script>
Angular
Với giải pháp này, bản thân widget vẫn sẽ là 1 angular app bình thường, những điểm cần lưu ý chỉ bắt đầu ở giai đoạn build script để có thể sẵn sàng sử dụng tại website.
Việc khai báo AppModule sẽ có 1 số thay đổi như sau:
import { createCustomElement } from '@angular/elements';
export class AppModule {
constructor(injector: Injector) {
const el = createCustomElement(AppComponent, { injector });
customElements.define('online-booking', el);
}
ngDoBootstrap() {}
}
Khi ứng dụng angular được build, mọi việc sẽ vẫn như mọi ứng dụng bình thường, tuy nhiên, để phục vụ cho khâu đóng gói script, ta sẽ phải thêm 1 số cấu hình tùy chọn khi build cũng như bổ sung 1 nodejs script nhỏ cho việc đóng gói.
ng build booking-widget --output-hashing none
const fs = require('fs-extra');
const concat = require('concat');
(async function build() {
const files = [
'./dist/apps/booking-widget/runtime-es2015.js',
'./dist/apps/booking-widget/polyfills-es2015.js',
'./dist/apps/booking-widget/main-es2015.js'
];
const es5Files = [
'./dist/apps/booking-widget/runtime-es5.js',
'./dist/apps/booking-widget/polyfills-es5.js',
'./dist/apps/booking-widget/main-es5.js'
];
await concat(files, './dist/booking-widget.js');
await concat(es5Files, './dist/booking-widget-es5.js');
await fs.ensureDir('./dist/assets/booking-widget');
await fs.copyFile('./dist/apps/booking-widget/styles.css', './dist/assets/booking-widget/styles.css');
})();
Mọi việc đã hoàn thành, để sử dụng trong ứng dụng website, ta sẽ thực hiện chèn script như sau:
<script src="./booking-widget.js"></script>
<script src="./booking-widget-es5.js" nomodule defer"></script>
<script>
const widgetElement = document.createElement('div');
widgetElement.innerHTML = `
<online-booking id="${id}" lang="${lang}" fullscreen="${true}" ></online-booking>`;
document.body.appendChild(widgetElement);
</script>
Về mặt testing, cả 2 giải pháp này đều hỗ trợ việc testing như những ứng dụng React và Angular thông thường.
Web component
Với giải pháp này đầu tiên ta cần hiểu về việc tạo ra 1 web component, define 1 custom element trên website. Toàn bộ sẽ được thực hiện trong embedded script đc chèn vào trong website cần sử dụng.
Khi chèn embedded script việc khởi tạo và chạy web component sẽ dựa trên 1 lifecycle được cung cấp 1 cách “native” từ trình duyệt như sau: