🦉 如何开始一个 Owl 项目 🦉
概述
每个软件项目都有其特定的需求。许多需求可以通过一些工具链来解决,例如:webpack
、vite
、CSS 预处理器、打包器、转译器等。
因此,启动一个项目通常并不简单。一些框架会提供自己的工具链来简化这一过程,但你又需要学习并整合这些工具。
Owl 的设计目标是可以不依赖任何工具链而运行。正因为如此,Owl 也可以很轻松地集成到现代的构建工具中。下面我们将讨论几种不同的项目启动方式。每种方式在不同场景下有各自的优势和劣势。
使用简单的 HTML 文件
最简单的项目结构如下:一个包含你所有代码的 JavaScript 文件。
我们可以创建如下目录结构:
hello_owl/
index.html
owl.js
app.js
owl.js
可以从官方最新发布版本下载:https://github.com/odoo/owl/releases 我们需要选择后缀为 .iife
的文件,它是为浏览器环境构建的,可以直接使用。
index.html
内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
<script src="owl.js"></script>
</head>
<body>
<script src="app.js"></script>
</body>
</html>
app.js
内容如下:
const { Component, mount, xml } = owl;
// Owl 组件
class Root extends Component {
static template = xml`<div>Hello Owl</div>`;
}
mount(Root, document.body);
只需用浏览器打开这个 HTML 文件,就能看到 “Hello Owl” 的欢迎信息。这个结构虽然简单,但足够用于快速原型开发。你也可以使用 Owl 的压缩版本来稍微优化它。
使用静态服务器
上面的结构有个明显的缺点:所有应用代码只能放在一个文件里。当然我们也可以用多个 <script>
标签来引入多个文件,但这会带来以下问题:
- 必须手动保证引入顺序正确
- 各模块内容必须暴露为全局变量
- 多文件之间无法自动补全(IDE 支持差)
一个“低技术门槛”的解决办法是使用 原生 JavaScript 模块(即 ESM)。但这需要一个前提:浏览器不允许通过 file://
协议加载模块,所以我们需要用静态服务器来运行页面。
我们可以创建如下项目结构:
hello_owl/
src/
index.html
main.js
owl.js
root.js
如前所述,owl.js
可以从 Owl 发布页 下载 .iife.js
版本。
index.html
内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
<script src="owl.js"></script>
</head>
<body>
<script src="main.js" type="module"></script>
</body>
</html>
请注意:main.js
被标记为 type="module"
,这意味着浏览器会将它作为模块来解析,并自动加载其依赖。
root.js
与 main.js
内容如下:
// root.js
const { Component, mount, xml } = owl;
export class Root extends Component {
static template = xml`<div>Hello Owl</div>`;
}
// main.js
import { Root } from "./root.js";
mount(Root, document.body);
这里 main.js
通过 import
引入 root.js
。注意必须保留 .js
后缀,否则浏览器会报错。
接下来我们需要通过静态服务器来运行项目。有以下两种方式:
方法一:使用 Python 简单服务器
$ cd src
$ python -m http.server 8022
# 页面可通过 http://localhost:8022 访问
方法二:使用 npm 项目(更 JS 友好)
在项目根目录添加如下 package.json
文件:
{
"name": "hello_owl",
"version": "0.1.0",
"description": "Starting Owl app",
"main": "src/index.html",
"scripts": {
"serve": "serve src"
},
"author": "John",
"license": "ISC",
"devDependencies": {
"serve": "^11.3.0"
}
}
然后运行以下命令:
npm install # 安装 serve 工具
npm run serve # 启动静态服务器
标准 JavaScript 项目
上面的做法适合快速开发或原型验证。但如果你需要以下功能:
- 实时热重载(livereload)
- 自动测试框架
- 将所有代码打包到单个文件
- 模块系统、自动补全、调试支持更强
就需要更完整的项目结构和工具支持。
以下是标准 Owl 项目的目录结构:
hello_owl/
public/
index.html
src/
components/
Root.js
main.js
tests/
components/
Root.test.js
helpers.js
.gitignore
package.json
webpack.config.js
public/
:放静态资源(如 HTML、图片)src/
:放组件、业务代码tests/
:放测试代码
index.html
内容:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello Owl</title>
</head>
<body></body>
</html>
这里没有 <script>
标签,它将由 Webpack 自动注入。
主要 JS 文件内容:
// src/components/Root.js
import { Component, xml, useState } from "@odoo/owl";
export class Root extends Component {
static template = xml`
<div t-on-click="update">
Hello <t t-esc="state.text"/>
</div>`;
state = useState({ text: "Owl" });
update() {
this.state.text = this.state.text === "Owl" ? "World" : "Owl";
}
}
// src/main.js
import { utils, mount } from "@odoo/owl";
import { Root } from "./components/Root";
mount(Root, document.body);
测试文件:
// tests/components/Root.test.js
import { Root } from "../../src/components/Root";
import { makeTestFixture, nextTick, click } from "../helpers";
import { mount } from "@odoo/owl";
let fixture;
beforeEach(() => {
fixture = makeTestFixture();
});
afterEach(() => {
fixture.remove();
});
describe("Root", () => {
test("Works as expected...", async () => {
await mount(Root, fixture);
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
click(fixture, "div");
await nextTick();
expect(fixture.innerHTML).toBe("<div>Hello World</div>");
});
});
测试辅助工具:
// tests/helpers.js
import { Component } from "@odoo/owl";
import "regenerator-runtime/runtime";
export async function nextTick() {
await new Promise((resolve) => setTimeout(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
}
export function makeTestFixture() {
let fixture = document.createElement("div");
document.body.appendChild(fixture);
return fixture;
}
export function click(elem, selector) {
elem.querySelector(selector).dispatchEvent(new Event("click"));
}
.gitignore 文件:
node_modules/
package-lock.json
dist/
package.json 文件:
{
"name": "hello_owl",
"version": "0.1.0",
"description": "Demo app",
"main": "src/index.html",
"scripts": {
"test": "jest",
"build": "webpack --mode production",
"dev": "webpack-dev-server --mode development"
},
"author": "Someone",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.8.4",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"babel-jest": "^25.1.0",
"babel-loader": "^8.0.6",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"html-webpack-plugin": "^3.2.0",
"jest": "^25.1.0",
"regenerator-runtime": "^0.13.3",
"serve": "^11.3.0",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.10.2"
},
"dependencies": {
"@odoo/owl": "^1.0.4"
},
"babel": {
"plugins": ["@babel/plugin-proposal-class-properties"],
"env": {
"test": {
"plugins": ["transform-es2015-modules-commonjs"]
}
}
},
"jest": {
"verbose": false,
"testRegex": "(/tests/.*(test|spec))\\.js?$",
"moduleFileExtensions": ["js"],
"transform": {
"^.+\\.[t|j]sx?$": "babel-jest"
}
}
}
webpack.config.js 配置:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const host = process.env.HOST || "localhost";
module.exports = function (env, argv) {
const mode = argv.mode || "development";
return {
mode: mode,
entry: "./src/main.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: "babel-loader",
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".js", ".jsx"],
},
devServer: {
contentBase: path.resolve(__dirname, "public/index.html"),
compress: true,
hot: true,
host,
port: 3000,
publicPath: "/",
},
plugins: [
new HtmlWebpackPlugin({
inject: true,
template: path.resolve(__dirname, "public/index.html"),
}),
],
};
};
运行命令:
npm run build # 构建生产环境版本,输出到 dist/
npm run dev # 启动开发服务器,支持热重载
npm run test # 运行 jest 测试套件
如需我为你生成这个项目的模板、代码或配置,欢迎继续告诉我!