开发 Discourse 插件 - 第 1 部分 - 创建基本插件

在 Discourse 中开发插件其实非常简单,只要你掌握了一些小技巧。本文的目标是创建一个插件骨架,并带你了解基础知识。

你的开发环境

确保你的电脑上已经运行着 Discourse 的开发环境。我建议你使用相应的安装指南,完成后回来继续。

plugin.rb

:tada: 使用 GitHub - discourse/discourse-plugin-skeleton: Template for Discourse plugins · GitHub 在你的 plugins 目录中创建一个完整的 Discourse 插件骨架 :tada:

该骨架现已集成在 Discourse 核心代码中,运行 rake plugin:create[plugin-name] 即可使用骨架创建插件。

当 Discourse 启动时,它会在 plugins 目录中查找包含 plugin.rb 文件的子目录。plugin.rb 文件有两个作用:它是你插件的清单,包含关于你插件的必要信息,如名称、联系信息和描述。第二个作用是初始化运行插件所需的任何 Ruby 代码。

在我们的例子中,我们不会添加任何 Ruby 代码,但仍需要 plugin.rb。让我们创建目录 basic-plugin,并在其中创建文件 plugin.rb,内容如下:

basic-plugin/plugin.rb

# name: basic-plugin
# about: A super simple plugin to demonstrate how plugins work
# version: 0.0.1
# authors: Awesome Plugin Developer
# url: https://github.com/yourusername/basic-plugin

创建此文件后,你应该重启本地服务器,插件应该会被加载。

一个重要的注意事项!

如果你习惯常规的 Rails 开发,你可能会注意到插件在热重载方面并不那么方便。通常,当你更改插件代码时,你应该 Ctrl+c 停止服务器运行,然后使用 bin/dev 重新启动它。

我的更改没有被应用! :warning:

有时缓存没有被完全清除,特别是在创建新文件或删除旧文件时。为了解决这个问题,请删除你的 tmp 文件夹并重新启动 Rails。在 Mac 上,你可以通过一条命令完成:rm -rf tmp; bin/dev

检查插件是否已加载

重启本地服务器后,访问 URL /admin/plugins(确保你以管理员身份登录,因为只有管理员才能看到插件注册表)。

如果一切正常,你应该在列表中看到你的插件:

恭喜你,你刚刚创建了第一个插件!

让我们添加一些 JavaScript

目前你的插件什么也没做。让我们添加一个 JavaScript 文件,当 Discourse 加载时弹出一个警告框。这会对任何用户造成极大的困扰,也不建议作为实际插件使用,但它将展示如何将 JavaScript 插入到我们的运行应用程序中。

创建以下文件:

plugins/basic-plugin/assets/javascripts/discourse/initializers/alert.js

export default {
  name: "alert",
  initialize() {
    alert("alert boxes are annoying!");
  },
};

现在如果你重启本地服务器,你应该会在屏幕上看到 “alert boxes are annoying!”。 (如果没有,请参阅上面的“我的更改没有被应用”部分)。

让我们逐步分析它是如何工作的:

  1. 放置在 assets/javascripts/discourse/initializers 中的 JavaScript 文件会在 Discourse 应用程序加载时自动执行。

  2. 这个特定的文件 export 了一个对象,该对象包含一个 name 和一个 initialize 函数。

  3. name 必须是唯一的,所以我把它命名为 alert

  4. 当应用程序加载时,会调用 initialize() 函数。在我们的例子中,它所做的只是执行我们的 alert() 代码。

你现在正式成为 Discourse 插件开发者了!


系列更多内容

第 1 部分:本文
第 2 部分:插件出口 (Plugin Outlets)
第 3 部分:站点设置 (Site Settings)
第 4 部分:Git 设置
第 5 部分:管理界面
第 6 部分:验收测试
第 7 部分:发布你的插件


本文档受版本控制 - 请在 GitHub 上建议更改。

110 个赞
Plugin API documentations?
How to create a new plugins?
Connecting to database with a plugin
Separating View from Data, Plugins, Modularity
Categories Topic Replies
How can I add "dislike" button?
Overwriting controllers in plugin
Create fully custom header
Added custom initializer
Which files can you override from a plugin?
Developing Discourse Plugins - Part 2 - Connect to a plugin outlet
Developing Discourse Plugins - Part 5 - Add an admin interface
(Superseded) Plugin Tutorial #1 - How to manipulate the text in the composer?
Which is Better? Discourse or Flarum?
Hide features for non-admin users through plugin
Customizing handlebars templates
How to properly display jalali dates for Persian language
Topic Ratings Plugin
Enable Like-Button for not logged-in visitors
Why do I need a block storage?
Custom Field not working
Rails plugin generator
Why GNU License?
Code reading help. ES6, plugin-outlet `_connectorCache`, custom-html `_customizations`, appEvents
Customizing handlebars templates
Application Files after Digital Ocean Setup
Application Files after Digital Ocean Setup
Application Files after Digital Ocean Setup
Make an external http request from discourse
Specify user by external id
How can I make my own Discourse plugins?
Reputation and level on member profile
How to Change User Profile Picture
Something like shortcodes for Discourse?
Calendar plugin features to make it really useful for us
Plugin Documentation Style Guide
Any options for over-riding the username restrictions?
How would updating effect custom overrides?
I would like to change the template completely
Adding a second tag description
Rails plugin generator
Creating a plugin
Fatal: Not a git repository (or any parent up to mount point /discourse)
Fatal: Not a git repository (or any parent up to mount point /discourse)
Adding command line tools support for user api keys
Discourse Code Modification for personal use
Plugin routing problems
[PAID] Mentorship needed
Can You Restrict Signups Using `swot` Email Verification?
Plugin API documentations?
Insert tag based upon word match
Triggering account creation/login on external service when a user logs in on discourse
Place for total coding beginners to learn how to customise?
Add additional button post menu
Fetch third party data for topic list
The Ignore User feature (now in 2.3.0.beta7)
Help with a plugin
Create Custom APIs
Proper Development and Deployment
How to make only a group of users can send messages to staff
How might we better structure #howto?
Theme-Component v Plugin: What's the difference
Creating Parsing Extensions
How can I customized my own site
All latest images in posts from a category
(Ruby) Selecting only posts that are made by non-original posters
Hosting discourse on our infrastructure AWS
Display FullName instead of UserName in the Profile
Calling Python scripts w/ arguments on the backend via plugin?
Extend Existing Controller?
How to add custom fields to models
Extend Existing Controller?
Bot writer's tip: Processing every post
Add user to group after purchase
Support for proposed plug-in
Autocompletion for a user field at signup
Custom discourse with limited features
Overriding user_guardian.rb in a plugin (no fork necessary!)
Problems with git clone
Friendly user profile url
Website Integration
Discourse developement environment setup
How to show Full Name in Embeds
How to display full name in comments embeds
Rake task "assets:precompile" is failing due to JS Compilation issue
Change code does not work in VS Studio code editor
Edit Code
Discourse Chain Topics Plugin
Contributing to Discourse
How to edit topics/show.html.erb
Install Discourse on macOS for development
Developing Discourse Plugins - Part 3 - Add custom site settings
Developing Discourse Plugins - Part 4 - Setup git
Developing Discourse Plugins - Part 7 - Publish your plugin
Developing Discourse Plugins - Part 6 - Add acceptance tests
Learn how to start building stuff for Discourse if you're newbie (like myself)
How to disable updating username for admins & moderators?
Backend code
Backend code
Install Discourse on Ubuntu or Debian for Development
How to make changes on backend side of discourse application using rails
Introduction to Discourse Development
Discourse blog (article and comments)
How to learn more about Discourse back-end?
I want to learn how to program for Discourse
Discourse Audio Message
Create custom Automations
How can one add a toggle button to the post menu and implement an action for that toggle button in a Discourse plugin?
Content box
Create an advertising page
Discourse 有自定义插件模板吗,如何写自定义插件呢
Deploy custom Discourse repository via docker
Multiple custom fields for topics similar to custom userfields
How do I go about making a very customized theme?
How can I create a Discourse that's just like Stemaway.com?
Adding new columns to user lists in groups
How to pin a post within a topic?
Ruby script to send automatic welcome message to new users
:cn: Installing the Discourse development environment on Ubuntu 在 Ubuntu 上安装 Discourse 开发环境
Admin not accessible because of auto-minify for Cloudflare
Store pdf and doc files as raw text in the database - Where to start?
Update Password Encryption Method
I'm trying to run discourse app locally in production env without docker
How to turn off "Body seems unclear, is it a complete sentence?'
Hosting dynamic pages with discourse
Invisible way to force display order of tags?
Controllers and Routes
Learn about Discourse client-side flow?
How do you learn to build Discourse plugins?
How do you learn to build Discourse plugins?
Admin not accessible because of auto-minify for Cloudflare
Customizing CSS via plugin
About the Plugin category
Visual diagraming to add visual dimension to Conversations?
Custom Onebox engine
How can I install my own plugin written in Ruby?
Allow reply-to individual instead of topic/forum (mailing list feature)
Adapt github changes to my own site
Push to digital ocean from command line and rebuild
How to convince my university to use/install Discourse?
Hack to enable invitations for Trust level 1 users
Dropcaps in Discourse - cannot override span tag
Getting certain posts to Zapier
Plugin: add a menu icon (next to search)
Error When creating custom plugin
Error When creating custom plugin
IOTA as a currency for Discourse
New columns in directory (/users)
Bootstrap fails when plugin file specifies gem version range
Running my own discourse image
How to add a job to cron/anacron when creating Docker container
How to transfer data from plugin to app/views templates?

I am able to create plugin in development, it works fine. Thanks.
But I don’t see a way to install other’s plugin in development
So first question is how can I fork someone’s plugin in development and then build on top of that(hope copy paste that repo is not the recommended way)?
Second question is, it seems my created plugin is part of my repo, is there a way to create a separate github repo for the plugin automatically which others can directly use to install?

1 个赞

I have a preferred method to do this. First, I have a ~/code directory where I have all my git projects. So I check out discourse and the plugin in that directory so it’s something like this:

~/code

discourse
discourse-some-plugin

Then, within the ~/code/discourse/plugins folder I create a symlink to the plugin:

$ cd ~/code/discourse/plugins
$ ln -s ~/code/discourse-some-plugin .

Then for good measure:

$ cd ~/code/discourse
$ rm -rf tmp
$ bundle exec rails server

Now you can fork the plugin in ~/code/discourse-some-plugin, make pull requests or whatever you want. It’ll be used by discourse.

13 个赞

What is the best way to render ajax response inside a widget?
I was able to console.log the ajax response, but it is not rendering inside the widget.

Tried to call this.scheduleRerender() after getting ajax response but both results in an infinite loop.

1 个赞

I am getting following error when trying to load a helper module inside my widget

Could you please help me to resolve this error?
Thanks

You should make sure your widget can contain state. After the ajax request, set it on the state object and trigger a this.scheduleRerender and it should appear. For an example look at how the post menu shows who liked something.

Looks like you have the wrong path to your helper. One way to see all the paths that Discourse has resolved is by typing require._eak_seen in your console. Look for the correct path name.

Thanks Robin Ward for your help. I was able to load my module after investigating with require._eak_seen.

Below is the code for my widget. The problem is that this.scheduleRerender() is causing an infinite loop. The div always shows loading animation (even without this.scheduleRerender) which seems this.state.loading is not being set.

import { createWidget } from 'discourse/widgets/widget';
import { getTopic } from 'discourse/plugins/my-plugin/discourse/helpers/topics';
import { ajax } from 'discourse/lib/ajax';
import { h } from 'virtual-dom';

export default createWidget('topic-widget', {
  tagName: 'div.my-topics',
  defaultState() {
    return { loading: false};
  },

  refreshTopic() {
    if (this.state.loading) { return; }
    this.state.loading = true;
    this.state.topic = 'empty';
    getTopic(this).then((result) => {
      console.log(result);
      this.state.topic = result;
      this.state.loading = false;
      this.scheduleRerender();
    });
  },

  html(attrs, state) {
    if (!state.topic) {
      this.refreshTopic();
    }
    const result = [];
    if (state.loading) {
      result.push(h('div.spinner-container', h('div.spinner')));
    } else if (state.topic !== 'empty') {
      result.push(state.topic);
    } else {
      result.push(h('div.no-messages', 'No topic.'))
    }

    return result;
  },
});

Could you help to resolve this error?

Your code looks mostly fine, however, all widgets that deal with state require a key attribute. You should have seen a warning about this, although maybe the warning only appears when running tests?

Try adding a buildKey function like buildKey: () => 'topic-widget'.

That should probably fix it. Also, a much less serious issue is you might want to add topic: null to your state object. Javascript is much faster when it knows the shape of the object in advance.

5 个赞

Thanks Robin Ward.
Adding buildKey function worked fine for me.

1 个赞

@eviltrout I got bitten by this in development and didn’t get a warning about setting a key attribute.

3 个赞

Ah, it only warns in Ember.testing:

https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/widgets/widget.js.es6#L153

We might want to just raise an error if that happens now? It is pretty dangerous for Widgets to do that.

4 个赞

:thumbsup: for raising an error instead. Maybe we can extend that to development too? :thought_balloon:

2 个赞

Hopefully this will prevent others from making this mistake:

https://github.com/discourse/discourse/commit/d4bbdcd7d63ddbf20239c95d1ba1cee9030d7e95

6 个赞

Hi,

Is this url correct? Shouldn’t this be ,

I’m new here. please correct me if im wrong :slight_smile:

In Discourse, mainly there are two big JavaScript sections “discourse” and “admin”. Also you can see few more in https://github.com/discourse/discourse/tree/master/app/assets/javascripts.

In plugins, to differentiate admin & normal user section JavaScript files we use “discourse” and “admin” keywords in between like below

plugins/basic-plugin/assets/javascripts/discourse/initializers/alert.js.es6
plugins/basic-plugin/assets/javascripts/admin/initializers/alert.js.es6


Also it will just work even without identification keywords like you mentioned :slight_smile:

11 个赞

@vinothkannans Thank you a lot for the explanation. :slight_smile:

I’m experiencing the same issue with getting symlinks to work (I’m on Linux) - the ln -s appears to work, but from inside the docker container the path is inaccessible. The plugin works when it is directly copied into the plugins folder, but I like the symlink workflow as it enables a more sensible Git arrangment.

This SO question appears to suggest that symlinks won’t work this way unless we also add the linked-to plugin volume to the docker run command https://stackoverflow.com/questions/38485607/mount-host-directory-with-a-symbolic-link-inside-in-docker-container

Anyone any thoughts on this? What are the pro plugin developers doing for a sensible Git workflow?

2 个赞

I’m using symlinks but my dev setup is Docker-frei.

Symlinks are a really nice way of being able to include or exclude plugins on a ‘build’ very quickly without disturbing the codebase.

2 个赞

Here’s what I’ve had to do in order to preserve a similar workflow to using symlinks (softlinks):

I edited /bin/docker/boot_dev in Discourse (outside the container) so that the plugin I want to work on is added as a volume and mounted in the /src/plugins/ directory (inside the container).

In my case it looks like this:

docker run -d -p 9405:9405 -p 1080:1080 -p 3000:3000 -p 9292:9292 \
-v "$DATA_DIR:/shared/postgres_data:delegated" \
-v "$SOURCE_DIR:/src:delegated" \ 
-v "/home/marcus/code/discourse/discourse-reflective-learning-plugin:/src/plugins/discourse-reflective-learning-plugin" \
$ENV_ARGS --hostname=discourse --name=discourse_dev --restart=always \
discourse/discourse_dev:release /sbin/boot

I’ve used absolute paths because I couldn’t be bothered to get into relative paths inside Docker and $SOURCE_DIR environment variables, but I’m sure there’s a cleverer way to do this, so that perhaps all your plugins could be in directories at the same level as discourse and it would automagically include them all. Hope this helps someone.

2 个赞

I am totally open to a PR that automatically follows symlinks in plugins/ dir and then smart mounts volumes.

4 个赞