Add new post about studio-frontend
This commit is contained in:
parent
802d162335
commit
9bd9499793
367
_posts/2018-04-26-studio-frontend.md
Normal file
367
_posts/2018-04-26-studio-frontend.md
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
---
|
||||||
|
title: "Studio-Frontend: Developing Frontend Separate from edX Platform"
|
||||||
|
layout: post
|
||||||
|
---
|
||||||
|
|
||||||
|
*This is a blog post that I originally wrote for the [edX engineering
|
||||||
|
blog](https://engineering.edx.org/).*
|
||||||
|
|
||||||
|
At the core of edX is the [edx-platform](https://github.com/edx/edx-platform), a
|
||||||
|
monolithic Django code-base 2.7 times the size of Django itself.
|
||||||
|
<!--excerpt-->
|
||||||
|
|
||||||
|
```
|
||||||
|
edx-platform (master)> tokei
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
Language Files Lines Code Comments Blanks
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
ActionScript 1 118 74 23 21
|
||||||
|
CoffeeScript 25 2296 1492 545 259
|
||||||
|
CSS 55 17106 14636 1104 1366
|
||||||
|
HTML 667 72608 36892 30307 5409
|
||||||
|
JavaScript 1471 458791 349670 54805 54316
|
||||||
|
JSON 91 14421 14421 0 0
|
||||||
|
JSX 28 2104 1792 45 267
|
||||||
|
LESS 1 949 606 232 111
|
||||||
|
Makefile 1 34 21 8 5
|
||||||
|
Markdown 24 331 331 0 0
|
||||||
|
Mustache 1 1 1 0 0
|
||||||
|
Python 3246 553766 438165 29089 86512
|
||||||
|
ReStructuredText 48 4258 4258 0 0
|
||||||
|
Sass 423 75509 55536 4548 15425
|
||||||
|
Shell 13 830 453 251 126
|
||||||
|
SQL 4 6158 4971 1171 16
|
||||||
|
Plain Text 151 2982 2982 0 0
|
||||||
|
TypeScript 20 88506 76800 11381 325
|
||||||
|
XML 364 5283 4757 231 295
|
||||||
|
YAML 36 1643 1370 122 151
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
Total 6670 1307694 1009228 133862 164604
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
```
|
||||||
|
|
||||||
|
35% of the edx-platform is JavaScript. While it has served edX well since its
|
||||||
|
inception in 2012, reaching over 11 million learners in thousands of courses on
|
||||||
|
[edX.org](https://www.edx.org/) and many more millions on all of the [Open edX
|
||||||
|
instances across the
|
||||||
|
world](https://openedx.atlassian.net/wiki/spaces/COMM/pages/162245773/Sites+powered+by+Open+edX),
|
||||||
|
it is starting to show its age. Most of it comes in the form of [Backbone.js
|
||||||
|
apps](http://backbonejs.org/) loaded by [RequireJS](http://requirejs.org/) in
|
||||||
|
Django [Mako templates](http://www.makotemplates.org/), with
|
||||||
|
[jQuery](https://jquery.com/) peppered throughout (and — yes — some 2296 lines
|
||||||
|
of [CoffeeScript](http://coffeescript.org/)).
|
||||||
|
|
||||||
|
Many valiant efforts are underway to modernize the frontend of edx-platform
|
||||||
|
including replacing RequireJS with Webpack, Backbone.js with
|
||||||
|
[React](https://reactjs.org/), and ES5 JavaScript and CoffeeScript with ES6
|
||||||
|
JavaScript. Many of these efforts [were covered in detail at the last Open edX
|
||||||
|
conference](https://www.youtube.com/watch?v=xicBnbDX4AY) and in [Open edX
|
||||||
|
Proposal 11: Front End Technology
|
||||||
|
Standards](https://open-edx-proposals.readthedocs.io/en/latest/oep-0011-bp-FED-technology.html).
|
||||||
|
However, the size and complexity of the edx-platform means that these kind of
|
||||||
|
efforts are hard to prioritize, and, in the meantime, frontend developers are
|
||||||
|
forced to [wait over 10
|
||||||
|
minutes](https://openedx.atlassian.net/wiki/spaces/FEDX/pages/264700138/Asset+Compilation+Audit+2017-11-01)
|
||||||
|
for our home-grown asset pipeline to build before they can view changes.
|
||||||
|
|
||||||
|
There have also been efforts to incrementally modularize and extract parts of
|
||||||
|
the edx-platform into separate python packages that could be installed as
|
||||||
|
[Django apps](https://docs.djangoproject.com/en/2.0/ref/applications/), or even
|
||||||
|
as separately deployed
|
||||||
|
[microservices](https://en.wikipedia.org/wiki/Microservices). This allows
|
||||||
|
developers to work independently from the rest of the organization inside of a
|
||||||
|
repository that they own, manage, and is small enough that they could feasibly
|
||||||
|
understand it entirely.
|
||||||
|
|
||||||
|
When my team was tasked with improving the user experience of pages in
|
||||||
|
[Studio](https://studio.edx.org/), the tool that course authors use to create
|
||||||
|
course content, we opted to take a similar architectural approach with the
|
||||||
|
frontend and create a new repository where we could develop new pages in
|
||||||
|
isolation and then integrate them back into the edx-platform as a plugin. We
|
||||||
|
named this new independent repository
|
||||||
|
[studio-frontend](https://github.com/edx/studio-frontend). With this approach,
|
||||||
|
our team owns the entire studio-frontend code-base and can make the best
|
||||||
|
architectural changes required for its features without having to consult with
|
||||||
|
and contend with all of the other teams at edX that contribute to the
|
||||||
|
edx-platform. Developers of studio-frontend can also avoid the platform’s slow
|
||||||
|
asset pipeline by doing all development within the studio-frontend repository
|
||||||
|
and then later integrating the changes into platform.
|
||||||
|
|
||||||
|
## React and Paragon
|
||||||
|
|
||||||
|
When edX recently started to conform our platform to the [Web Content
|
||||||
|
Accessibility Guidelines 2.0 AA (WCAG 2.0
|
||||||
|
AA)](https://www.w3.org/WAI/intro/wcag), we faced many challenges in
|
||||||
|
retrofitting our existing frontend code to be accessible. Rebuilding Studio
|
||||||
|
pages from scratch in studio-frontend allows us to not only follow the latest
|
||||||
|
industry standards for building robust and performant frontend applications, but
|
||||||
|
to also build with accessibility in mind from the beginning.
|
||||||
|
|
||||||
|
The Javascript community has made great strides recently to [address
|
||||||
|
accessibility issues in modern web
|
||||||
|
apps](https://reactjs.org/docs/accessibility.html). However, we had trouble
|
||||||
|
finding an open-source React component library that fully conformed to WCAG 2.0
|
||||||
|
AA and met all of edX’s needs, so we decided to build our own:
|
||||||
|
[Paragon](https://github.com/edx/paragon).
|
||||||
|
|
||||||
|
Paragon is a library of building-block components like buttons, inputs, icons,
|
||||||
|
and tables which were built from scratch in React to be accessible. The
|
||||||
|
components are styled using the [Open edX theme of Bootstrap
|
||||||
|
v4](https://github.com/edx/edx-bootstrap) (edX’s decision to adopt Bootstrap is
|
||||||
|
covered in
|
||||||
|
[OEP-16](https://open-edx-proposals.readthedocs.io/en/latest/oep-0016-bp-adopt-bootstrap.html)).
|
||||||
|
Users of Paragon may also choose to use the
|
||||||
|
[themeable](https://github.com/edx/paragon#export-targets) unstyled target and
|
||||||
|
provide their own Bootstrap theme.
|
||||||
|
|
||||||
|
|
||||||
|
![Paragon's modal component displayed in
|
||||||
|
Storybook](/img/blog/paragon-modal-storybook.png)
|
||||||
|
|
||||||
|
Studio-frontend composes together Paragon components into higher-level
|
||||||
|
components like [an accessibility
|
||||||
|
form](https://github.com/edx/studio-frontend/blob/master/src/accessibilityIndex.jsx)
|
||||||
|
or [a table for course assets with searching, filtering, sorting, pagination,
|
||||||
|
and upload](https://github.com/edx/studio-frontend/blob/master/src/index.jsx).
|
||||||
|
While we developed these components in studio-frontend, we were able to improve
|
||||||
|
the base Paragon components. Other teams at edX using the same components were
|
||||||
|
able to receive the same improvements with a single package update.
|
||||||
|
|
||||||
|
![Screenshot of the studio-frontend assets table inside of
|
||||||
|
Studio](/img/blog/studio-frontend-assets-table.png)
|
||||||
|
|
||||||
|
## Integration with Studio
|
||||||
|
|
||||||
|
We were able to follow the typical best practices for developing a React/Redux
|
||||||
|
application inside studio-frontend, but at the end of the day, we still had to
|
||||||
|
somehow get our components inside of existing Studio pages and this is where
|
||||||
|
most of the challenges arose.
|
||||||
|
|
||||||
|
## Webpack
|
||||||
|
|
||||||
|
The aforementioned move from RequireJS to Webpack in the edx-platform made it
|
||||||
|
possible for us to build our studio-frontend components from source with Webpack
|
||||||
|
within edx-platform. However, this approach tied us to the edx-platform’s slow
|
||||||
|
asset pipeline. If we wanted rapid development, we had to duplicate the
|
||||||
|
necessary Webpack config between both studio-frontend and edx-platform.
|
||||||
|
|
||||||
|
Instead, studio-frontend handles building the development and production Webpack
|
||||||
|
builds itself. In development mode, the incremental rebuild that happens
|
||||||
|
automatically when a file is changed takes under a second. The production
|
||||||
|
JavaScript and CSS bundles, which take about 25 seconds to build, are published
|
||||||
|
with every new release to
|
||||||
|
[NPM](https://www.npmjs.com/package/@edx%2Fstudio-frontend). The edx-platform
|
||||||
|
`npm install`s studio-frontend and then copies the built production files from
|
||||||
|
`node_modules` into its Django static files directory where the rest of the
|
||||||
|
asset pipeline will pick it up.
|
||||||
|
|
||||||
|
To actually use the built JavaScript and CSS, edx-platform still needs to
|
||||||
|
include it in its Mako templates. We made a [Mako template
|
||||||
|
tag](https://github.com/edx/edx-platform/blob/master/common/djangoapps/pipeline_mako/templates/static_content.html#L93-L122)
|
||||||
|
that takes a Webpack entry point name in studio-frontend and generates script
|
||||||
|
tags that include the necessary files from the studio-frontend package. It also
|
||||||
|
dumps all of the initial context that studio-frontend needs from the
|
||||||
|
edx-platform Django app into [a JSON
|
||||||
|
object](https://github.com/edx/edx-platform/blob/master/cms/templates/asset_index.html#L36-L56)
|
||||||
|
in a script tag on the page that studio-frontend components can access via a
|
||||||
|
shared id. This is how studio-frontend components get initial data from Studio,
|
||||||
|
like which course it’s embedded in.
|
||||||
|
|
||||||
|
For performance, modules that are shared across all studio-frontend components
|
||||||
|
are extracted into `common.min.js` and `common.min.css` files that are included
|
||||||
|
on every Studio template that has a studio-frontend component. User's browsers
|
||||||
|
should cache these files so that they do not have to re-download libraries like
|
||||||
|
React and Redux every time they visit a new page that contains a studio-frontend
|
||||||
|
component.
|
||||||
|
|
||||||
|
## CSS Isolation
|
||||||
|
|
||||||
|
Since the move to Bootstrap had not yet reached the Studio part of the
|
||||||
|
edx-platform, most of the styling clashed with the Bootstrap CSS that
|
||||||
|
studio-frontend components introduced. And, the Bootstrap styles were also
|
||||||
|
leaking outside of the studio-frontend embedded component `div` and affecting
|
||||||
|
the rest of the Studio page around it.
|
||||||
|
|
||||||
|
![Diagram of a studio-frontend component embedded inside of
|
||||||
|
Studio](/img/blog/studio-frontend-style-isolation.png)
|
||||||
|
|
||||||
|
We were able to prevent styles leaking outside of the studio-frontend component
|
||||||
|
by scoping all CSS to only the `div` that wraps the component. Thanks to the
|
||||||
|
Webpack [postcss-loader](https://github.com/postcss/postcss-loader) and the
|
||||||
|
[postcss-prepend-selector](https://github.com/ledniy/postcss-prepend-selector)
|
||||||
|
we were able to automatically scope all of our CSS selectors to that `div` in
|
||||||
|
our build process.
|
||||||
|
|
||||||
|
Preventing the Studio styles from affecting our studio-frontend component was a
|
||||||
|
much harder problem because it means avoiding the inherently cascading nature of
|
||||||
|
CSS. A common solution to this issue is to place the 3rd-party component inside
|
||||||
|
of an
|
||||||
|
[`iframe`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe)
|
||||||
|
element, which essentially creates a completely separate sub-page where both CSS
|
||||||
|
and JavaScript are isolated from the containing page. Because `iframe`s
|
||||||
|
introduce many other performance and styling issues, we wanted to find a
|
||||||
|
different solution to isolating CSS.
|
||||||
|
|
||||||
|
The CSS style [`all:
|
||||||
|
initial`](https://developer.mozilla.org/en-US/docs/Web/CSS/all) allows
|
||||||
|
resetting all properties on an element to their initial values as defined in the
|
||||||
|
CSS spec. Placing this style under a wildcard selector in studio-frontend
|
||||||
|
allowed us to reset all inherited props from the legacy Studio styles without
|
||||||
|
having to enumerate them all by hand.
|
||||||
|
|
||||||
|
```css
|
||||||
|
* {
|
||||||
|
all: initial;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
While this CSS property doesn’t have broad browser support yet, we were able to
|
||||||
|
polyfill it thanks to postcss with the
|
||||||
|
[postcss-initial](https://github.com/maximkoretskiy/postcss-initial) plugin.
|
||||||
|
|
||||||
|
However, this resets the styles to *nothing*. For example, all `div`s are
|
||||||
|
displayed inline. To return the styles back to to some sane browser default we
|
||||||
|
had to re-apply a browser default stylesheet. You can read more about this
|
||||||
|
technique at
|
||||||
|
[default-stylesheet](https://github.com/thallada/default-stylesheet).
|
||||||
|
|
||||||
|
From there, Bootstrap’s
|
||||||
|
[reboot](https://getbootstrap.com/docs/4.0/content/reboot/) normalizes the
|
||||||
|
browser-specific styling to a common baseline and then applies the Bootstrap
|
||||||
|
styles conflict-free from the surrounding CSS cascade.
|
||||||
|
|
||||||
|
There's a candidate recommendation in CSS for a [`contains`
|
||||||
|
property](https://www.w3.org/TR/css-contain-1/), which will "allow strong,
|
||||||
|
predictable isolation of a subtree from the rest of the page". I hope that it
|
||||||
|
will provide a much more elegant solution to this problem once browsers support
|
||||||
|
it.
|
||||||
|
|
||||||
|
## Internationalization
|
||||||
|
|
||||||
|
Another major challenge with separating out the frontend from edx-platform was
|
||||||
|
that most of our internationalization tooling was instrumented inside the
|
||||||
|
edx-platform. So, in order to display text in studio-frontend components in the
|
||||||
|
correct language we either had to pass already-translated strings from the
|
||||||
|
edx-platform into studio-frontend, or set-up translations inside
|
||||||
|
studio-frontend.
|
||||||
|
|
||||||
|
We opted for the latter because it kept the content close to the code that used
|
||||||
|
it. Every display string in a component is stored in a
|
||||||
|
[displayMessages.jsx](https://github.com/edx/studio-frontend/blob/master/src/components/AssetsTable/displayMessages.jsx)
|
||||||
|
file and then imported and referenced by an id within the component. A periodic
|
||||||
|
job extracts these strings from the project, pushes them up to our translations
|
||||||
|
service [Transifex](https://www.transifex.com/), and pulls any new translations
|
||||||
|
to store them in our NPM package.
|
||||||
|
|
||||||
|
Because Transifex’s `KEYVALUEJSON` file format does not allow for including
|
||||||
|
comments in the strings for translation, [Eric](https://github.com/efischer19)
|
||||||
|
created a library called [reactifex](https://github.com/efischer19/reactifex)
|
||||||
|
that will send the comments in separate API calls.
|
||||||
|
|
||||||
|
Studio includes the user’s language in the context that it sends a
|
||||||
|
studio-frontend component for initialization. Using this, the component can
|
||||||
|
display the message for that language if it exists. If it does not, then it will
|
||||||
|
display the original message in English and [wrap it in a `span` with `lang="en"`
|
||||||
|
as an
|
||||||
|
attribute](https://github.com/edx/studio-frontend/blob/master/src/utils/i18n/formattedMessageWrapper.jsx)
|
||||||
|
so that screen-readers know to read it in English even if their default is some
|
||||||
|
other language.
|
||||||
|
|
||||||
|
Read more about studio-frontend’s internationalization process in [the
|
||||||
|
documentation that Eric
|
||||||
|
wrote](https://github.com/edx/studio-frontend/blob/master/src/data/i18n/README.md).
|
||||||
|
|
||||||
|
## Developing with Docker
|
||||||
|
|
||||||
|
To normalize the development environment across the whole studio-frontend team,
|
||||||
|
development is done in a Docker container. This is a minimal Ubuntu 16.04
|
||||||
|
container with specific version of Node 8 installed and its only purpose is to
|
||||||
|
run Webpack. This follows the pattern established in [OEP-5: Pre-built
|
||||||
|
Development
|
||||||
|
Environments](https://open-edx-proposals.readthedocs.io/en/latest/oep-0005-arch-containerize-devstack.html)
|
||||||
|
for running a single Docker container per process that developers can easily
|
||||||
|
start without installing dependencies.
|
||||||
|
|
||||||
|
Similar to edX’s [devstack](https://github.com/edx/devstack) there is a Makefile
|
||||||
|
with commands to start and stop the docker container. The docker container then
|
||||||
|
immediately runs [`npm run
|
||||||
|
start`](https://github.com/edx/studio-frontend/blob/master/package.json#L12),
|
||||||
|
which runs Webpack with the
|
||||||
|
[webpack-dev-server](https://github.com/webpack/webpack-dev-server). The
|
||||||
|
webpack-dev-server is a node server that serves assets built by Webpack.
|
||||||
|
[Studio-frontend's Webpack
|
||||||
|
config](https://github.com/edx/studio-frontend/blob/master/config/webpack.dev.config.js#L94)
|
||||||
|
makes this server available to the developer's host machine
|
||||||
|
at `http://localhost:18011`.
|
||||||
|
|
||||||
|
With [hot-reload](https://webpack.js.org/concepts/hot-module-replacement/)
|
||||||
|
enabled, developers can now visit that URL in their browser, edit source files
|
||||||
|
in studio-frontend, and then see changes reflected instantly in their browser
|
||||||
|
once Webpack finishes its incremental rebuild.
|
||||||
|
|
||||||
|
However, many studio-frontend components need to be able to talk to the
|
||||||
|
edx-platform Studio backend Django server. Using [docker’s network connect
|
||||||
|
feature](https://docs.docker.com/compose/networking/#use-a-pre-existing-network)
|
||||||
|
the studio-frontend container can join the developer’s existing docker devstack
|
||||||
|
network so that the studio-frontend container can make requests to the docker
|
||||||
|
devstack Studio container at `http://edx.devstack.studio:18010/` and Studio can
|
||||||
|
access studio-frontend at `http://dahlia.studio-fronend:18011/`.
|
||||||
|
|
||||||
|
The webpack-dev-server can now [proxy all
|
||||||
|
requests](https://github.com/edx/studio-frontend/blob/master/config/webpack.dev.config.js#L101)
|
||||||
|
to Studio API endpoints (like `http://localhost:18011/assets`)
|
||||||
|
to `http://edx.devstack.studio:18010/`.
|
||||||
|
|
||||||
|
## Developing within Docker Devstack Studio
|
||||||
|
|
||||||
|
Since studio-frontend components will be embedded inside of an existing Studio
|
||||||
|
page shell, it’s often useful to develop on studio-frontend containers inside of
|
||||||
|
this set-up. [This can be
|
||||||
|
done](https://github.com/edx/studio-frontend#development-inside-devstack-studio)
|
||||||
|
by setting a variable in the devstack's `cms/envs/private.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
STUDIO_FRONTEND_CONTAINER_URL = 'http://localhost:18011'
|
||||||
|
```
|
||||||
|
|
||||||
|
This setting is checked in the Studio Mako templates wherever studio-frontend
|
||||||
|
components are embedded. If it is set to a value other than `None`, then the
|
||||||
|
templates will request assets from that URL instead of the Studio's own static
|
||||||
|
assets directory. When a developer loads a Studio page with an embedded
|
||||||
|
studio-frontend component, their studio-frontend webpack-dev-server will be
|
||||||
|
requested at that URL. Similarly to developing on studio-frontend in isolation,
|
||||||
|
edits to source files will trigger a Webpack compilation and the Studio page
|
||||||
|
will be hot-reloaded or reloaded to reflect the changes automatically.
|
||||||
|
|
||||||
|
Since the studio-frontend JS loaded on `localhost:18010` is now requesting the
|
||||||
|
webpack-dev-server on `localhost:18011`,
|
||||||
|
an [`Access-Control-Allow-Origin` header](https://github.com/edx/studio-frontend/blob/master/config/webpack.dev.config.js#L98)
|
||||||
|
has to be configured on the webpack-dev-server to get around CORS violations.
|
||||||
|
|
||||||
|
![Diagram of studio-frontend's docker container communicating to Studio inside
|
||||||
|
of the devstack_default docker
|
||||||
|
network](/img/blog/studio-frontend-docker-devstack.png)
|
||||||
|
|
||||||
|
## Deploying to Production
|
||||||
|
|
||||||
|
[Each release of
|
||||||
|
studio-frontend](https://github.com/edx/studio-frontend#releases) will upload
|
||||||
|
the `/dist` files built by Webpack in production mode to
|
||||||
|
[NPM](https://www.npmjs.com/package/@edx/studio-frontend). edx-platform
|
||||||
|
requires a particular version of studio-frontend in its
|
||||||
|
[`package.json`](https://github.com/edx/edx-platform/blob/master/package.json#L7).
|
||||||
|
When a new release of edx-platform is made, `paver update_assets` will run
|
||||||
|
which will copy all of the files in the
|
||||||
|
`node_modules/@edx/studio-frontend/dist/` to the Studio static folder.
|
||||||
|
Because `STUDIO_FRONTEND_CONTAINER_URL` will be `None` in production, it will be
|
||||||
|
ignored, and Studio pages will request studio-frontend assets from that static
|
||||||
|
folder.
|
||||||
|
|
||||||
|
## Future
|
||||||
|
|
||||||
|
Instead of “bringing the new into the old”, we’d eventually like to move to a
|
||||||
|
model where we “work in the new and bring in the old if necessary”. We could
|
||||||
|
host studio-frontend statically on a completely separate server which talks to
|
||||||
|
Studio via a REST (or [GraphQL](https://graphql.org/)) API. This approach would
|
||||||
|
eliminate the complexity around CSS isolation and bring big performance wins for
|
||||||
|
our users, but it would require us to rewrite more of Studio.
|
BIN
img/blog/paragon-modal-storybook.png
Normal file
BIN
img/blog/paragon-modal-storybook.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 82 KiB |
BIN
img/blog/studio-frontend-assets-table.png
Normal file
BIN
img/blog/studio-frontend-assets-table.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 200 KiB |
BIN
img/blog/studio-frontend-docker-devstack.png
Normal file
BIN
img/blog/studio-frontend-docker-devstack.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
img/blog/studio-frontend-style-isolation.png
Normal file
BIN
img/blog/studio-frontend-style-isolation.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
Loading…
Reference in New Issue
Block a user