Skip to content

🦉 如何开始一个 Owl 项目 🦉

概述

每个软件项目都有其特定的需求。许多需求可以通过一些工具链来解决,例如:webpackvite、CSS 预处理器、打包器、转译器等。

因此,启动一个项目通常并不简单。一些框架会提供自己的工具链来简化这一过程,但你又需要学习并整合这些工具。

Owl 的设计目标是可以不依赖任何工具链而运行。正因为如此,Owl 也可以很轻松地集成到现代的构建工具中。下面我们将讨论几种不同的项目启动方式。每种方式在不同场景下有各自的优势和劣势。


使用简单的 HTML 文件

最简单的项目结构如下:一个包含你所有代码的 JavaScript 文件。

我们可以创建如下目录结构:

hello_owl/
  index.html
  owl.js
  app.js

owl.js 可以从官方最新发布版本下载:https://github.com/odoo/owl/releases 我们需要选择后缀为 .iife 的文件,它是为浏览器环境构建的,可以直接使用。

index.html 内容如下:

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 内容如下:

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 内容如下:

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.jsmain.js 内容如下:

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 简单服务器

bash
$ cd src
$ python -m http.server 8022
# 页面可通过 http://localhost:8022 访问

方法二:使用 npm 项目(更 JS 友好)

在项目根目录添加如下 package.json 文件:

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"
  }
}

然后运行以下命令:

bash
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 内容:

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Hello Owl</title>
  </head>
  <body></body>
</html>

这里没有 <script> 标签,它将由 Webpack 自动注入。

主要 JS 文件内容:

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);

测试文件:

js
// 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>");
  });
});

测试辅助工具:

js
// 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 文件:

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 配置:

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"),
      }),
    ],
  };
};

运行命令:

bash
npm run build   # 构建生产环境版本,输出到 dist/
npm run dev     # 启动开发服务器,支持热重载
npm run test    # 运行 jest 测试套件

如需我为你生成这个项目的模板、代码或配置,欢迎继续告诉我!

本站内容仅供学习与参考