Porter
中文版
Porter is a consolidated browser module solution which provides a module system for web browsers that is both CommonJS and ES Modules compatible, with following features supported:
- Both synchronous and asynchronous module loading are supported, which means
require()
, require.async()
, import
, or import()
can be used at will to request modules, dynamically imported modules will be bundled separately. - Bundle at package level, or bundle everything, it's completely up to you.
- Fast enough module resolution and transpilation, with reasonable levels of cache that makes production builds more effecient.
It is recommended to first start with our starter documentation or the thorough user guides.
Setup
This document is mostly about Porter the middleware. To learn about Porter CLI, please visit the corresponding folder or the Porter Documentation.
Porter the middleware is compatible with Koa (both major versions) and Express:
const Koa = require('koa')
const Porter = require('@cara/porter')
const app = new Koa()
const porter = new Porter()
app.use(porter.async())
app.use(porter.func())
Modules
With the default setup, browser modules at ./components
folder is now accessible with /path/to/file.js
. Take examples/cli for example, the file structure shall resemble that of below:
➜ cli git:(master) tree -L 2
.
├── components
│ ├── app.css
│ └── app.js
├── node_modules
│ ├── @cara
│ │ └── porter
│ ├── jquery
│ └── prismjs
├── package.json
└── public
└── index.html
In ./public/index.html
, we can now add CSS and JavaScript entries:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>A Porter Demo</title>
<link rel="stylesheet" type="text/css" href="/app.css">
</head>
<body>
<h1>A Porter Demo</h1>
<script src="/app.js?main"></script>
</body>
</html>
The extra ?main
parameter in the JavaScript entry query is added for historical reasons. It tells the porter middleware to include loader.js when bundling app.js, which isn't necessary if loader.js is included explicitly:
<script src="/loader.js" data-main="app.js"></script>
<script src="/loader.js"></script>
<script>porter.import('app')</script>
Both formats are no longer recommended, please use <script src="/app.js?main"></script>
directly.
In JavaScript entry, all kinds of imports are supported:
import $ from 'jquery';
import * as React from 'react';
import util from './util';
import './foo.css';
import wasm from './foo.wasm';
import Worker from 'worker-loader!./worker.js';
In CSS entry, there's @import
:
@import "prismjs/themes/prism.css";
@import "./base.css";
Options
In a nutshell, here is the list of porter options:
const path = require('path');
const Porter = require('@cara/porter');
const porter = new Porter({
root: process.cwd(),
paths: 'components',
output: {
path: 'public',
},
cache: {
path: '.porter-cache',
identifier({ packet }) {
return JSON.stringify([
require('@cara/porter/package.json').version,
packet.transpiler,
packet.transpilerVersion,
packet.transpilerOpts,
]);
},
},
preload: [ 'preload', '@babel/runtime' ],
resolve: {
alias: {
'@': path.join(process.cwd(), 'components'),
},
extensions: [ '*', '.js', '.jsx', '.ts', '.tsx', '.css' ],
import: [
{ libraryName: 'antd', style: 'css' },
{ libraryName: 'lodash',
libraryDirectory: '',
camel2DashComponentName: false },
],
},
transpile: {
include: [ 'antd' ],
},
bundle: {
exclude: [ 'antd' ],
},
source: {
serve: process.env.NODE_ENV !== 'production',
root: 'localhost:3000',
},
});
Deployment
It is possible (and also recommended) to disable Porter in production, as long as the assets are compiled with porter.compileAll()
. To compile assets of the project, simply call porter.compileAll({ entries })
:
const porter = new Porter({
output: { path: 'dist' },
});
await porter.compileAll({
entries: ['app.js', 'app.css']
});
Porter will compile entries and their dependencies, bundle them together afterwards. How the modules are bundled is a simple yet complicated question. Here's the default bundling strategy:
- Entries are bundled separately, e.g.
entries: ['app.js', 'app2.js']
are compiled into two different bundles. - Dependencies are bundled per package with internal modules put together, e.g. jQuery gets compiled as
jquery/3.3.1/dist/jquery.4f8208b0.js
. - Dependencies with multiple entries gets bundled per package as well, e.g. lodash methods will be compiled as
lodash/4.17.10/lodash.36bdcd6d.js
.
Take following app.js for example:
import $ from 'jquery';
import throttle from 'lodash/throttle';
import camelize from 'lodash/camelize';
import util from './util';
When porter.compileAll({ entries: ['app.js'] })
is done, the output files should be:
public
├── app.${contenthash}.js
├── app.${contenthash}.js.map
├── jquery
│ └── 3.3.1
│ └── dist
| ├── jquery.${contenthash}.js
| └── jquery.${contenthash}.js.map
└── lodash
└── 4.17.10
├── ~bundle.${contenthash}.js
└── ~bundle.${contenthash}.js.map
For different kinds of projects, different strategies shall be employed. We can tell Porter to bundle dependencies at certain scope with porter.compileEntry()
:
porter.compileEntry('app.js', { package: true })
porter.compileEntry('app.js', { all: true })