feat: migrate to http and ky-universal

master
Pooya Parsa 2019-04-09 14:01:27 +04:30 committed by GitHub
commit d3e2c085d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 9195 additions and 1907 deletions

View File

@ -1,7 +1,7 @@
<!--
IMPORTANT: Please use the following link to create a new issue:
https://cmty.app/nuxt/issues/new?repo=axios-module
https://cmty.app/nuxt/issues/new?repo=http-module
If your issue was not created using the app above, it will be closed immediately.
-->

View File

@ -1,4 +1,4 @@
# 📦 HTTP Module
# HTTP Module
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
@ -7,25 +7,7 @@
[![Dependencies][david-dm-src]][david-dm-href]
[![Standard JS][standard-js-src]][standard-js-href]
TODO
## ✅ Features
✓ Automatically set base URL for client & server side
✓ Exposes `setToken` function to `$axios` so we can easily and globally set authentication tokens
✓ Automatically enables `withCredentials` when requesting to base URL
✓ Proxy request headers in SSR (Useful for auth)
✓ Integrated with Nuxt.js Progressbar while making requests (TODO)
✓ Integrated with [Proxy Module](https://github.com/nuxt-community/proxy-module)
✓ Auto retry requests
📖 [**Read Documentation**](https://http.nuxtjs.org) (TODO)
📖 [**Read Documentation**](https://http.nuxtjs.org)
## Development

View File

@ -1,11 +0,0 @@
module.exports = {
presets: [
[
'@babel/preset-env', {
targets: {
esmodules: true
}
}
]
]
}

View File

@ -1,5 +0,0 @@
module.exports = {
extends: [
'@commitlint/config-conventional'
]
}

1
docs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.vuepress/dist

29
docs/.vuepress/config.js Normal file
View File

@ -0,0 +1,29 @@
module.exports = {
title: 'HTTP Module',
description: 'Universal HTTP Module for Nuxt',
themeConfig: {
repo: 'nuxt/http',
docsDir: 'docs',
editLinks: true,
displayAllHeaders: true,
sidebar: [
{
collapsable: false,
children: [
'/',
'setup',
'usage',
'options',
'advanced',
'migration'
]
}
],
nav: [
{
text: 'Release Notes',
link: 'https://github.com/nuxt/http/blob/dev/CHANGELOG.md'
}
]
}
}

View File

@ -1,30 +0,0 @@
# 📦 Axios Module
> Secure and Easy [Axios](https://github.com/mzabriskie/axios) integration with Nuxt.js.
## Features
✓ Automatically set base URL for client & server side
✓ Exposes `setToken` function to `$axios` so we can easily and globally set authentication tokens
✓ Automatically enables `withCredentials` when requesting to base URL
✓ Proxy request headers in SSR (Useful for auth)
✓ Fetch Style requests
✓ Integrated with Nuxt.js Progressbar while making requests
✓ Integrated with [Proxy Module](https://github.com/nuxt-community/proxy-module)
✓ Auto retry requests with [axios-retry](https://github.com/softonic/axios-retry)
## Links
* [GitHub](https://github.com/nuxt/http-module)
* [Release Notes](./CHANGELOG.md)
* [Migration Guide](migration.md)
* [Examples](https://axios.nuxtjs.org/usage.html)
> 👉 To get started head to [Setup](setup.md) section.

View File

@ -1,9 +0,0 @@
# Summary
* [Setup](setup.md)
* [Usage](usage.md)
* [Extending axios](extend.md)
* [Helpers](helpers.md)
* [Options](options.md)
* [Migration Guide](migration.md)
* [Changelog](../CHANGELOG.md)

101
docs/advanced.md Normal file
View File

@ -0,0 +1,101 @@
# Advanced
## Hooks
Sometimes we want to globally intercept HTTP request and responses.
for example display a toast on error or log them or dynamically modify requests.
HTTP module provides helpers to register hooks for request lifecycle:
- `onRequest(config)`
- `onResponse(response)`
- `onError(err)` (`err.response` may be available on response errors)
These functions don't have to return anything by default.
### Register Hooks
For registering hooks, you have to create a nuxt plugin:
**nuxt.config.js**
```js
{
modules: [
'@nuxt/http',
],
plugins: [
'~/plugins/http'
]
}
```
**plugins/http.js**
```js
export default function ({ $http }) {
$http.onRequest(config => {
console.log('Making request to ' + config.url)
})
$http.onError(error => {
if(error.response.status === 500) {
alert('Request Error!')
}
})
}
```
## Header Helpers
### `setHeader(name, value)`
Globally set a header to all subsequent requests.
> NOTE: This method should not be called inside hooks as it is global
Parameters:
* **name**: Name of the header
* **value**: Value of the header
```js
// Add header `Authorization: 123` to all requests
this.$http.setHeader('Authorization', '123')
// Override `Authorization` header with new value
this.$http.setHeader('Authorization', '456')
// Add header `Content-Type: application/x-www-form-urlencoded`
this.$http.setHeader('Content-Type', 'application/x-www-form-urlencoded')
// Remove default Content-Type header
this.$http.setHeader('Content-Type', false)
```
### `setToken(token, type)`
Globally set `Authorization` header to all subsequent requests.
Parameters:
* **token**: Authorization token
* **type**: Authorization token prefix, usually `Bearer`. Defaults to nothing
```js
// Adds header: `Authorization: 123` to all requests
this.$http.setToken('123')
// Overrides `Authorization` header with new value
this.$http.setToken('456')
// Adds header: `Authorization: Bearer 123` to all requests
this.$http.setToken('123', 'Bearer')
// Adds header: `Authorization: Bearer 123` to only post and delete requests
this.$http.setToken('123', 'Bearer', ['post', 'delete'])
// Removes default Authorization header
this.$http.setToken(false)
```

View File

@ -1,34 +0,0 @@
## Extending Axios
If you need to customize axios by registering interceptors and changing global config, you have to create a nuxt plugin.
**nuxt.config.js**
```js
{
modules: [
'@nuxt/http',
],
plugins: [
'~/plugins/axios'
]
}
```
**plugins/axios.js**
```js
export default function ({ $axios, redirect }) {
$axios.onRequest(config => {
console.log('Making request to ' + config.url)
})
$axios.onError(error => {
const code = parseInt(error.response && error.response.status)
if (code === 400) {
redirect('/400')
}
})
}
```

View File

@ -1,96 +0,0 @@
## Helpers
### Interceptors
Axios plugin provides helpers to register axios interceptors easier and faster.
- `onRequest(config)`
- `onResponse(response)`
- `onError(err)`
- `onRequestError(err)`
- `onResponseError(err)`
These functions don't have to return anything by default.
Example: (`plugins/axios.js`)
```js
export default function ({ $axios, redirect }) {
$axios.onError(error => {
if(error.response.status === 500) {
redirect('/sorry')
}
})
}
```
### Fetch Style requests
Axios plugin also supports fetch style requests with `$` prefixed methods:
```js
// Normal usage with axios
let data = (await $axios.get('...')).data
// Fetch Style
let data = await $axios.$get('...')
```
### `setHeader(name, value, scopes='common')`
Axios instance has a helper to easily set any header.
Parameters:
* **name**: Name of the header
* **value**: Value of the header
* **scopes**: Send only on specific type of requests. Defaults
* Type: _Array_ or _String_
* Defaults to `common` meaning all types of requests
* Can be `get`, `post`, `delete`, ...
```js
// Adds header: `Authorization: 123` to all requests
this.$axios.setHeader('Authorization', '123')
// Overrides `Authorization` header with new value
this.$axios.setHeader('Authorization', '456')
// Adds header: `Content-Type: application/x-www-form-urlencoded` to only post requests
this.$axios.setHeader('Content-Type', 'application/x-www-form-urlencoded', [
'post'
])
// Removes default Content-Type header from `post` scope
this.$axios.setHeader('Content-Type', false, ['post'])
```
### `setToken(token, type, scopes='common')`
Axios instance has an additional helper to easily set global authentication header.
Parameters:
* **token**: Authorization token
* **type**: Authorization token prefix(Usually `Bearer`).
* **scopes**: Send only on specific type of requests. Defaults
* Type: _Array_ or _String_
* Defaults to `common` meaning all types of requests
* Can be `get`, `post`, `delete`, ...
```js
// Adds header: `Authorization: 123` to all requests
this.$axios.setToken('123')
// Overrides `Authorization` header with new value
this.$axios.setToken('456')
// Adds header: `Authorization: Bearer 123` to all requests
this.$axios.setToken('123', 'Bearer')
// Adds header: `Authorization: Bearer 123` to only post and delete requests
this.$axios.setToken('123', 'Bearer', ['post', 'delete'])
// Removes default Authorization header from `common` scope (all requests)
this.$axios.setToken(false)
```

View File

@ -0,0 +1,53 @@
# Migration Guide
If you are migrating from [axios-module](https://github.com/nuxt-community/axios-module) this guide may be useful.
- There is no scope for `setHeader`, `setToken`. Scope is common which means being applied to all requests.
- `onRequestError` and `onResponseError` hooks removed. Use `onError` instead.
- `debug` option has been removed. You can setup a basic logger using `onRequest` hook.
- The is no longer progress bar integration due to the lack of support from `fetch` spec. This option may be back after KY support of [`onProgress`](https://github.com/sindresorhus/ky/pull/34)
This module is using [ky](https://github.com/sindresorhus/ky) amd [fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). There are breaking changes for usage and making requests.
## Parsing response body
Despite axios that does this automatically, you have to call specific methods to parse reponse body.
```diff
-- const resJson = await this.$axios.get('/url')
++ const resJson = await this.$http.get('/url').json()
```
There is also a shortcut for JSON by using `$` prefix on request method name.
```js
const resJson = await this.$http.$get('/url')
```
Supported response types:
- `json`
- `text`
- `formData`
- `arrayBuffer`
- `blob`
## Sending requests with body
Despire axios, fetch and ky always accept **two** arguments for making requests (input and options). You have to pass request body in options:
For plain data or `Body`:
```diff
-- this.$axios.post('/url', 'some data')
++ this.$http.post('/url', { body: 'some data' })
```
For JSON:
```diff
-- this.$axios.post('/url', { name: 'foo' })
++ this.$http.post('/url', { json: { name: 'foo' } })
```
* `json` is a shortcut to `body` that sets `content-type` header and serializes JSON object.

View File

@ -1,8 +1,16 @@
## Options
# Options
You can pass options using module options or `axios` section in `nuxt.config.js`
You can pass options using module options or `http` section in `nuxt.config.js`
### `prefix`, `host` and `port`
```js
{
http: {
// HTTP options here
}
}
```
## `prefix`, `host`, `port`
This options are used for default values of `baseURL` and `browserBaseURL`.
@ -10,7 +18,7 @@ Can be customized with `API_PREFIX`, `API_HOST` (or `HOST`) and `API_PORT` (or `
Default value of `prefix` is `/`.
### `baseURL`
## `baseURL`
* Default: `http://[HOST]:[PORT][PREFIX]`
@ -20,7 +28,7 @@ Environment variable `API_URL` can be used to **override** `baseURL`.
**Note:** `baseURL` and `proxy` doesn't work together, you need to use `prefix` instead.
### `browserBaseURL`
## `browserBaseURL`
* Default: `baseURL` (or `prefix` when `options.proxy` is enabled)
@ -28,29 +36,17 @@ Base URL which is used and prepended to make requests in client side.
Environment variable `API_URL_BROWSER` can be used to **override** `browserBaseURL`.
### `https`
## `https`
* Default: `false`
If set to `true`, `http://` in both `baseURL` and `browserBaseURL` will be changed into `https://`.
### `progress`
* Default: `true`
Integrate with Nuxt.js progress bar to show a loading bar while making requests. (Only on browser, when loading bar is available.)
You can also disable progress bar per request using `progress` config.
```js
this.$axios.$get('URL', { progress: false })
```
### `proxy`
## `proxy`
* Default: `false`
You can easily integrate Axios with [Proxy Module](https://github.com/nuxt-community/proxy-module) and is much recommended to prevent CORS and deployment problems.
You can easily integrate HTTP with [Proxy Module](https://github.com/nuxt-community/proxy-module) and is much recommended to prevent CORS and deployment problems.
**nuxt.config.js**
@ -60,7 +56,7 @@ You can easily integrate Axios with [Proxy Module](https://github.com/nuxt-commu
'@nuxt/http'
],
axios: {
http: {
proxy: true // Can be also an object with default options
},
@ -81,44 +77,31 @@ proxy: {
}
```
### `retry`
## `retry`
* Default: `false`
Automatically intercept failed requests and retries them whenever posible using [axios-retry](https://github.com/softonic/axios-retry).
Automatically intercept failed requests and retry before failing.
By default, number of retries will be **3 times**, if `retry` value is set to `true`. You can change it by passing an object like this:
By default, number of retries will be **2 times**, if `retry` value is set to `true`. You can change it by passing an object like this:
```js
axios: {
retry: { retries: 3 }
http: {
retry: 1
}
```
### `credentials`
* Default: `false`
Adds an interceptor to automatically set `withCredentials` config of axios when requesting to `baseURL`
which allows passing authentication headers to backend.
### `debug`
* Default: `false`
Adds interceptors to log request and responses.
### `proxyHeaders`
## `proxyHeaders`
* Default: `true`
In SSR context, sets client request header as axios default request headers.
In SSR context, sets client request header as http default request headers.
This is useful for making requests which need cookie based auth on server side.
Also helps making consistent requests in both SSR and Client Side code.
> **NOTE:** If directing requests at a url protected by CloudFlare's CDN you should set this to false to prevent CloudFlare from mistakenly detecting a reverse proxy loop and returning a 403 error.
### `proxyHeadersIgnore`
## `proxyHeadersIgnore`
* Default `['host', 'accept']`

10
docs/package.json Normal file
View File

@ -0,0 +1,10 @@
{
"private": true,
"scripts": {
"dev": "vuepress dev",
"build": "vuepress build"
},
"devDependencies": {
"vuepress": "^1.0.0-alpha.44"
}
}

15
docs/readme.md Normal file
View File

@ -0,0 +1,15 @@
# Introduction
HTTP module for Nuxt.js provides a universal way to make HTTP requests to the API backend.
This module is a successor of [Axios Module](https://github.com/nuxt-community/axios-module) and behind the scenes use [ky-universal](https://github.com/sindresorhus/ky-universal) and [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to make HTTP requests. Please see [migration guide](./migration) if currently using axios module.
Starting with v2.5.0, Nuxt.js has built-in support for universal fetch. Using this module has serveral advantages and is mondatory in most of real-world use cases.
- Fluent [ky](https://github.com/sindresorhus/ky) API with more enhancenments and shortcuts
- Highly customizable options support for BaseURL
- Automatically proxy cookies and headers when making requests from server side
- Best practices to avoid token sharing while making server side requests
- Easy proxy support to avoid CORS problems and making deployment easier

View File

@ -1,5 +1,4 @@
## Setup
# Setup
Install with yarn:
@ -21,7 +20,7 @@ module.exports = {
'@nuxt/http',
],
axios: {
http: {
// proxyHeaders: false
}
}

View File

@ -1,35 +1,92 @@
## Usage
# Usage
### Component
## Making Requests
**`asyncData`**
Available HTTP methods:
- `get`
- `post`
- `put`
- `patch`
- `head`
- `delete`
For making a request use `$http.<method>(<url>, <options>)`. Returns a Promise that either rejects in case of network errors or resolves to a [Reponse](https://developer.mozilla.org/en-US/docs/Web/API/Response) object. You can use methods to convert response stream into usable data:
- `json`
- `text`
- `formData`
- `arrayBuffer`
- `blob`
**Example: Fetch a json file**
```js
async asyncData({ $axios }) {
const ip = await $axios.$get('http://icanhazip.com')
await $http.get('https://unpkg.com/nuxt/package.json').json()
```
Alternatively for json only you can use `$` prefixed shortcut:
```js
await $http.$get('https://unpkg.com/nuxt/package.json')
```
See [ky](https://github.com/sindresorhus/ky) docs for all available options.
### Sending Body
For sending body alongside with request, you can use either `json` or `body` options.
`json` is a shortcut that serializes object using `JSON.stringify` and also sets appreciate `content-type` header.
**Example: Post with JSON body**
```js
await $http.post('http://api.con', { json: { foo: 'bar' }})
```
**Example: Post with FormData body**
```js
const data = new FormData()
data.append('name', 'foo')
await $http.post('http://api.com/submit', { data })
```
## Using in `asyncData`
For `asyncData` and `fetch` you can access instance from context:
```js
async asyncData({ $http }) {
const ip = await $http.get('http://icanhazip.com').text()
return { ip }
}
```
**`methods`/`created`/`mounted`/etc**
## Using in Component Methods
Where you have access to `this`, you can use `this.$http`:
```js
methods: {
async fetchSomething() {
const ip = await this.$axios.$get('http://icanhazip.com')
const ip = await this.$http.get('http://icanhazip.com').text()
this.ip = ip
}
}
```
### Store actions (including `nuxtServerInit`)
## Using in Store
For store action you can also use `this.$http`:
```js
// In store
{
actions: {
async getIP ({ commit }) {
const ip = await this.$axios.$get('http://icanhazip.com')
const ip = await this.$http.get('http://icanhazip.com').text()
commit('SET_IP', ip)
}
}

7233
docs/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
module.exports = {
hooks: {
'commit-msg': 'commitlint -E HUSKY_GIT_PARAMS',
'pre-commit': 'yarn lint',
'pre-push': 'yarn lint'
}
}

View File

@ -1,11 +1,11 @@
const path = require('path')
const consola = require('consola')
const logger = consola.withScope('nuxt:axios')
const logger = consola.withScope('nuxt:http')
function axiosModule(_moduleOptions) {
function httpModule(_moduleOptions) {
// Combine options
const moduleOptions = { ...this.options.axios, ..._moduleOptions }
const moduleOptions = { ...this.options.http, ..._moduleOptions }
// Default port
const defaultPort =
@ -35,9 +35,6 @@ function axiosModule(_moduleOptions) {
const options = {
baseURL: `http://${defaultHost}:${defaultPort}${prefix}`,
browserBaseURL: null,
credentials: false,
debug: false,
progress: true,
proxyHeaders: true,
proxyHeadersIgnore: ['accept', 'host', 'cf-ray', 'cf-connecting-ip'],
proxy: false,
@ -65,7 +62,9 @@ function axiosModule(_moduleOptions) {
// Normalize options
if (options.retry === true) {
options.retry = {}
options.retry = 2
} else if (!options.retry) {
options.retry = 0
}
// Convert http:// to https:// if https option is on
@ -78,7 +77,7 @@ function axiosModule(_moduleOptions) {
// Register plugin
this.addPlugin({
src: path.resolve(__dirname, 'plugin.js'),
fileName: 'axios.js',
fileName: 'http.js',
options
})
@ -90,12 +89,12 @@ function axiosModule(_moduleOptions) {
])
}
// Set _AXIOS_BASE_URL_ for dynamic SSR baseURL
process.env._AXIOS_BASE_URL_ = options.baseURL
// Set _HTTP_BASE_URL_ for dynamic SSR baseURL
process.env._HTTP_BASE_URL_ = options.baseURL
logger.debug(`baseURL: ${options.baseURL}`)
logger.debug(`browserBaseURL: ${options.browserBaseURL}`)
}
module.exports = axiosModule
module.exports = httpModule
module.exports.meta = require('../package.json')

View File

@ -1,202 +1,102 @@
import Axios from 'axios'
<% if (options.retry) { %>import axiosRetry from 'axios-retry'<% } %>
import KY from 'ky-universal'
// Axios.prototype cannot be modified
const axiosExtra = {
setHeader (name, value, scopes = 'common') {
for (let scope of Array.isArray(scopes) ? scopes : [ scopes ]) {
if (!value) {
delete this.defaults.headers[scope][name];
return
}
this.defaults.headers[scope][name] = value
class HTTP {
constructor(defaults, ky = KY) {
this._defaults = {
hooks: {},
headers: {},
retry: 0,
...defaults
}
},
setToken (token, type, scopes = 'common') {
this._ky = ky
}
setHeader(name, value) {
if (!value) {
delete this._defaults.headers[name];
} else {
this._defaults.headers[name] = value
}
}
setToken(token, type) {
const value = !token ? null : (type ? type + ' ' : '') + token
this.setHeader('Authorization', value, scopes)
},
this.setHeader('Authorization', value)
}
_hook(name, fn) {
if (!this._defaults.hooks[name]) {
this._defaults.hooks[name] = []
}
this._defaults.hooks[name].push(fn)
}
onRequest(fn) {
this.interceptors.request.use(config => fn(config) || config)
},
this._hook('beforeRequest', fn)
}
onResponse(fn) {
this.interceptors.response.use(response => fn(response) || response)
},
onRequestError(fn) {
this.interceptors.request.use(undefined, error => fn(error) || Promise.reject(error))
},
onResponseError(fn) {
this.interceptors.response.use(undefined, error => fn(error) || Promise.reject(error))
},
this._hook('afterResponse', fn)
}
onError(fn) {
this.onRequestError(fn)
this.onResponseError(fn)
this._hook('onError', fn)
}
}
// Request helpers ($get, $post, ...)
for (let method of ['request', 'delete', 'get', 'head', 'options', 'post', 'put', 'patch']) {
axiosExtra['$' + method] = function () { return this[method].apply(this, arguments).then(res => res && res.data) }
}
for (let method of ['get', 'post', 'put', 'patch', 'head', 'delete']) {
HTTP.prototype[method] = async function (input, options) {
const _options = { ...this._defaults, ...options }
const extendAxiosInstance = axios => {
for (let key in axiosExtra) {
axios[key] = axiosExtra[key].bind(axios)
}
}
if (/^https?/.test(input)) {
delete _options.prefixUrl
}
<% if (options.debug) { %>
const log = (level, ...messages) => console[level]('[Axios]', ...messages)
const setupDebugInterceptor = axios => {
// request
axios.onRequestError(error => {
log('error', 'Request error:', error)
})
// response
axios.onResponseError(error => {
log('error', 'Response error:', error)
})
axios.onResponse(res => {
log(
'info',
'[' + (res.status + ' ' + res.statusText) + ']',
'[' + res.config.method.toUpperCase() + ']',
res.config.url)
if (process.browser) {
console.log(res)
} else {
console.log(JSON.stringify(res.data, undefined, 2))
}
return res
})
}<% } %>
<% if (options.credentials) { %>
const setupCredentialsInterceptor = axios => {
// Send credentials only to relative and API Backend requests
axios.onRequest(config => {
if (config.withCredentials === undefined) {
if (!/^https?:\/\//i.test(config.url) || config.url.indexOf(config.baseURL) === 0) {
config.withCredentials = true
try {
const response = await this._ky[method](input, _options)
return response
} catch (error) {
// Call onError hook
if (_options.hooks.onError) {
_options.hooks.onError.forEach(fn => fn(error))
}
// Throw error
throw error
}
})
}<% } %>
<% if (options.progress) { %>
const setupProgress = (axios, ctx) => {
if (process.server) {
return
}
// A noop loading inteterface for when $nuxt is not yet ready
const noopLoading = {
finish: () => { },
start: () => { },
fail: () => { },
set: () => { }
HTTP.prototype['$' + method] = function (input, options) {
return this[method](input, options).then(res => res.json())
}
const $loading = () => (window.$nuxt && window.$nuxt.$loading && window.$nuxt.$loading.set) ? window.$nuxt.$loading : noopLoading
let currentRequests = 0
axios.onRequest(config => {
if (config && config.progress === false) {
return
}
currentRequests++
})
axios.onResponse(response => {
if (response && response.config && response.config.progress === false) {
return
}
currentRequests--
if (currentRequests <= 0) {
currentRequests = 0
$loading().finish()
}
})
axios.onError(error => {
if (error && error.config && error.config.progress === false) {
return
}
currentRequests--
$loading().fail()
$loading().finish()
})
const onProgress = e => {
if (!currentRequests) {
return
}
const progress = ((e.loaded * 100) / (e.total * currentRequests))
$loading().set(Math.min(100, progress))
}
axios.defaults.onUploadProgress = onProgress
axios.defaults.onDownloadProgress = onProgress
}<% } %>
}
export default (ctx, inject) => {
// baseURL
const baseURL = process.browser
// prefixUrl
const prefixUrl = process.browser
? '<%= options.browserBaseURL %>'
: (process.env._AXIOS_BASE_URL_ || '<%= options.baseURL %>')
: (process.env._HTTP_BASE_URL_ || '<%= options.baseURL %>')
// Create fresh objects for all default header scopes
// Axios creates only one which is shared across SSR requests!
// https://github.com/mzabriskie/axios/blob/master/lib/defaults.js
const headers = {
common : {
'Accept': 'application/json, text/plain, */*'
},
delete: {},
get: {},
head: {},
post: {},
put: {},
patch: {}
}
const axiosOptions = {
baseURL,
headers
// Defaults
const defaults = {
retry: <%= parseInt(options.retry) %>,
prefixUrl
}
<% if (options.proxyHeaders) { %>
// Proxy SSR request headers headers
axiosOptions.headers.common = (ctx.req && ctx.req.headers) ? Object.assign({}, ctx.req.headers) : {}
<% for (let h of options.proxyHeadersIgnore) { %>delete axiosOptions.headers.common['<%= h %>']
defaults.headers = (ctx.req && ctx.req.headers) ? { ...ctx.req.headers } : {}
<% for (let h of options.proxyHeadersIgnore) { %>delete defaults.headers['<%= h %>']
<% } %><% } %>
if (process.server) {
// Don't accept brotli encoding because Node can't parse it
axiosOptions.headers.common['Accept-Encoding'] = 'gzip, deflate'
defaults.headers['Accept-Encoding'] = 'gzip, deflate'
}
// Create new axios instance
const axios = Axios.create(axiosOptions)
// Create new HTTP instance
const http = new HTTP(defaults)
// Extend axios proto
extendAxiosInstance(axios)
// Setup interceptors
<% if (options.debug) { %>setupDebugInterceptor(axios) <% } %>
<% if (options.credentials) { %>setupCredentialsInterceptor(axios)<% } %>
<% if (options.progress) { %>setupProgress(axios, ctx) <% } %>
<% if (options.retry) { %>axiosRetry(axios, <%= serialize(options.retry) %>)<% } %>
// Inject axios to the context as $axios
ctx.$axios = axios
inject('axios', axios)
// Inject http to the context as $http
ctx.$http = http
inject('http', http)
}

View File

@ -1,14 +1,12 @@
{
"name": "@nuxt/http",
"version": "0.0.0",
"description": "HTTP Module for Nuxt.js",
"description": "Universal HTTP Module for Nuxt.js",
"license": "MIT",
"contributors": [
"Pooya Parsa <pooya@pi0.ir>"
],
"author": "Pooya Parsa <pooya@pi0.ir>",
"main": "lib/module.js",
"types": "types/index.d.ts",
"repository": "https://github.com/nuxt/http-module",
"repository": "nuxt/http",
"publishConfig": {
"access": "public"
},
@ -24,15 +22,13 @@
],
"dependencies": {
"@nuxtjs/proxy": "^1.3.3",
"axios": "^0.18.0",
"axios-retry": "^3.1.2",
"consola": "^2.5.6"
"consola": "^2.5.6",
"ky": "^0.9.0",
"ky-universal": "^0.1.0"
},
"devDependencies": {
"@babel/core": "latest",
"@babel/preset-env": "latest",
"@commitlint/cli": "latest",
"@commitlint/config-conventional": "latest",
"@nuxtjs/eslint-config": "latest",
"babel-eslint": "latest",
"babel-jest": "latest",
@ -45,9 +41,8 @@
"eslint-plugin-promise": "latest",
"eslint-plugin-standard": "latest",
"eslint-plugin-vue": "latest",
"husky": "latest",
"jest": "latest",
"nuxt-edge": "latest",
"nuxt-edge": "^2.5.2-25896971.3b85dd97",
"standard-version": "latest"
}
}

29
test/_utils.js Normal file
View File

@ -0,0 +1,29 @@
const { Nuxt, Builder } = require('nuxt-edge')
const defaultConfig = require('./fixture/nuxt.config')
jest.setTimeout(60000)
async function setupNuxt(config) {
const nuxt = new Nuxt({
...defaultConfig,
...config
})
// Spy addTemplate
nuxt.moduleContainer.addTemplate = jest.fn(nuxt.moduleContainer.addTemplate)
const builder = new Builder(nuxt)
await builder.validatePages()
await builder.generateRoutesAndFiles()
nuxt.builder = builder
await nuxt.ready()
return nuxt
}
module.exports = {
setupNuxt
}

View File

@ -1,169 +0,0 @@
jest.setTimeout(60000)
const { Nuxt, Builder } = require('nuxt-edge')
const axios = require('axios')
const config = require('./fixture/nuxt.config')
let nuxt, addTemplate
const url = path => `http://localhost:3000${path}`
const setupNuxt = async (config) => {
nuxt = new Nuxt(config)
// Spy addTemplate
addTemplate = nuxt.moduleContainer.addTemplate = jest.fn(
nuxt.moduleContainer.addTemplate
)
const build = new Builder(nuxt)
await build.validatePages()
await build.generateRoutesAndFiles()
await nuxt.listen(3000)
}
const testSuite = () => {
test('baseURL', () => {
expect(addTemplate).toBeDefined()
const call = addTemplate.mock.calls.find(args => args[0].src.includes('plugin.js'))
const options = call[0].options
expect(options.baseURL.toString()).toBe('http://localhost:3000/test_api')
expect(options.browserBaseURL.toString()).toBe('/test_api')
})
test('asyncData', async () => {
const html = (await axios.get(url('/asyncData'))).data
expect(html).toContain('foo/bar')
})
test('mounted', async () => {
const window = await nuxt.renderAndGetWindow(url('/mounted'))
window.onNuxtReady(() => {
const html = window.document.body.innerHTML
expect(html).toContain('foo/bar')
})
})
test('init', async () => {
const window = await nuxt.renderAndGetWindow(url('/mounted'))
window.onNuxtReady(() => {
const $axios = window.$nuxt.$axios
expect($axios.defaults.xsrfHeaderName).toBe('X-CSRF-TOKEN')
})
})
test('ssr', async () => {
const makeReq = login => axios
.get(url('/ssr' + (login ? '?login' : '')))
.then(r => r.data)
.then(h => /session-[0-9]+/.exec(h))
.then(m => (m && m[0] ? m[0] : null))
const a = await makeReq()
const b = await makeReq(true)
const c = await makeReq()
const d = await makeReq(true)
expect(a).toBeNull()
expect(b).not.toBeNull()
expect(c).toBeNull() // Important!
expect(d).not.toBeNull()
expect(b).not.toBe(d)
})
test('ssr no brotli', async () => {
const makeReq = login => axios
.get(url('/ssr' + (login ? '?login' : '')))
.then(r => r.data)
.then(h => /encoding-\$(.*)\$/.exec(h))
.then(m => (m && m[1] ? m[1] : null))
const result = await makeReq()
expect(result).toBe('gzip, deflate')
})
}
describe('module', () => {
beforeAll(async () => {
nuxt = new Nuxt(config)
// Spy addTemplate
addTemplate = nuxt.moduleContainer.addTemplate = jest.fn(
nuxt.moduleContainer.addTemplate
)
await new Builder(nuxt).build()
await nuxt.listen(3000)
})
afterAll(async () => {
await nuxt.close()
})
testSuite()
})
describe('other options', () => {
beforeAll(async () => {
config.axios = {
prefix: '/test_api',
proxy: {},
credentials: true,
https: true,
retry: false
}
await setupNuxt(config)
})
afterAll(async () => {
await nuxt.close()
})
testSuite()
})
describe('browserBaseURL', () => {
beforeAll(async () => {
config.axios = {
browserBaseURL: '/test_api'
}
await setupNuxt(config)
})
afterAll(async () => {
await nuxt.close()
})
test('custom', () => {
expect(addTemplate).toBeDefined()
const call = addTemplate.mock.calls.find(args => args[0].src.includes('plugin.js'))
const options = call[0].options
expect(options.baseURL.toString()).toBe('http://localhost:3000/')
expect(options.browserBaseURL.toString()).toBe('/test_api')
})
})
describe('empty config', () => {
beforeAll(async () => {
config.axios = {}
await setupNuxt(config)
})
afterAll(async () => {
await nuxt.close()
})
test('preset baseURL and browserBaseURL', () => {
expect(addTemplate).toBeDefined()
const call = addTemplate.mock.calls.find(args => args[0].src.includes('plugin.js'))
const options = call[0].options
expect(options.baseURL.toString()).toBe('http://localhost:3000/')
expect(options.browserBaseURL.toString()).toBe('http://localhost:3000/')
})
})

25
test/baseURL.test.js Normal file
View File

@ -0,0 +1,25 @@
const { setupNuxt } = require('./_utils')
describe('browserBaseURL', () => {
let nuxt
test('setup', async () => {
nuxt = await setupNuxt({
http: {
browserBaseURL: '/test_api'
}
})
})
afterAll(async () => {
await nuxt.close()
})
test('custom', () => {
expect(nuxt.moduleContainer.addTemplate).toBeDefined()
const call = nuxt.moduleContainer.addTemplate.mock.calls.find(args => args[0].src.includes('plugin.js'))
const options = call[0].options
expect(options.baseURL).toBe('http://localhost:3000/')
expect(options.browserBaseURL).toBe('/test_api')
})
})

23
test/empty-config.test.js Normal file
View File

@ -0,0 +1,23 @@
const { setupNuxt } = require('./_utils')
describe('empty config', () => {
let nuxt
test('setup', async () => {
nuxt = await setupNuxt({
http: {}
})
})
afterAll(async () => {
await nuxt.close()
})
test('preset baseURL and browserBaseURL', () => {
expect(nuxt.moduleContainer.addTemplate).toBeDefined()
const call = nuxt.moduleContainer.addTemplate.mock.calls.find(args => args[0].src.includes('plugin.js'))
const options = call[0].options
expect(options.baseURL.toString()).toBe('http://localhost:3000/')
expect(options.browserBaseURL.toString()).toBe('http://localhost:3000/')
})
})

View File

@ -8,15 +8,18 @@ module.exports = {
resourceHints: false
},
modules: [
{ handler: require('../../') }
require('../..')
],
serverMiddleware: ['~/api.js'],
axios: {
serverMiddleware: [
'~/api.js'
],
http: {
prefix: '/test_api',
proxy: true,
credentials: true,
debug: true,
retry: true
retry: 1
},
plugins: ['~/plugins/axios']
build: {
terser: false
},
plugins: ['~/plugins/http']
}

View File

@ -7,7 +7,7 @@
<script>
export default {
async asyncData({ app }) {
let res = await app.$axios.$get('foo/bar')
const res = await app.$http.$get('foo/bar')
return {
res
}

View File

@ -14,7 +14,7 @@ export default {
async mounted() {
// Request with full url becasue we are in JSDom env
this.res = await this.$axios.$get('http://localhost:3000/test_api/foo/bar')
this.res = await this.$http.$get('http://localhost:3000/test_api/foo/bar')
}
}
</script>

View File

@ -1,29 +1,29 @@
<template>
<div>
<div>session-{{ axiosSessionId }}</div>
<div>encoding-${{ axiosEncoding }}$</div>
<div>session-{{ httpSessionId }}</div>
<div>encoding-${{ httpEncoding }}$</div>
</div>
</template>
<script>
// This will be intentically shared across requests
let reqCtr = 1
// This will be intentically shared across requests
let reqCtr = 1
export default {
async fetch({app, route}) {
let doLogin = route.query.login !== undefined
if (doLogin) {
app.$axios.setHeader('sessionId', reqCtr++)
}
export default {
computed: {
httpSessionId() {
return this.$http._defaults.headers.sessionId
},
computed: {
axiosSessionId() {
return this.$axios.defaults.headers.common.sessionId
},
axiosEncoding() {
return this.$axios.defaults.headers.common['Accept-Encoding']
}
httpEncoding() {
return this.$http._defaults.headers['Accept-Encoding']
}
},
fetch({ app, route }) {
const doLogin = route.query.login !== undefined
if (doLogin) {
app.$http.setHeader('sessionId', reqCtr++)
}
}
}
</script>

View File

@ -1,8 +0,0 @@
export default function ({ $axios, redirect }) {
$axios.onRequest((config) => {
// eslint-disable-next-line no-console
console.log('SPY: ' + config.url)
$axios.defaults.xsrfHeaderName = 'X-CSRF-TOKEN'
})
}

View File

@ -0,0 +1,8 @@
export default function ({ $http, redirect }) {
$http.setHeader('xsrfHeaderName', 'X-CSRF-TOKEN')
$http.onRequest((options) => {
// eslint-disable-next-line no-console
console.log('Request:', JSON.stringify(options))
})
}

View File

@ -1,12 +1,12 @@
export default {
actions: {
nuxtServerInit({ commit }, ctx) {
if (!ctx.$axios) {
throw new Error('$axios is not defined!')
if (!ctx.$http) {
throw new Error('$http is not defined!')
}
if (!ctx.app.$axios) {
throw new Error('$axios is not defined!')
if (!ctx.app.$http) {
throw new Error('$http is not defined!')
}
}
}

76
test/module.test.js Normal file
View File

@ -0,0 +1,76 @@
const fetch = require('node-fetch')
const { setupNuxt } = require('./_utils')
const url = path => `http://localhost:3000${path}`
describe('module', () => {
let nuxt
test('setup', async () => {
nuxt = await setupNuxt()
await nuxt.builder.build()
await nuxt.listen(3000)
})
afterAll(async () => {
await nuxt.close()
})
test('baseURL', () => {
expect(nuxt.moduleContainer.addTemplate).toBeDefined()
const call = nuxt.moduleContainer.addTemplate.mock.calls.find(args => args[0].src.includes('plugin.js'))
const options = call[0].options
expect(options.baseURL.toString()).toBe('http://localhost:3000/test_api')
expect(options.browserBaseURL.toString()).toBe('/test_api')
})
test('asyncData', async () => {
const html = await fetch(url('/asyncData')).then(r => r.text())
expect(html).toContain('foo/bar')
})
test('mounted', async () => {
const window = await nuxt.renderAndGetWindow(url('/mounted'))
window.onNuxtReady(() => {
const html = window.document.body.innerHTML
expect(html).toContain('foo/bar')
})
})
test('init', async () => {
const window = await nuxt.renderAndGetWindow(url('/mounted'))
window.onNuxtReady(() => {
const $http = window.$nuxt.$http
expect($http._defaults.xsrfHeaderName).toBe('X-CSRF-TOKEN')
})
})
test('ssr', async () => {
const makeReq = login => fetch(url('/ssr' + (login ? '?login' : '')))
.then(r => r.text())
.then(h => /session-[0-9]+/.exec(h))
.then(m => (m && m[0] ? m[0] : null))
const a = await makeReq()
const b = await makeReq(true)
const c = await makeReq()
const d = await makeReq(true)
expect(a).toBeNull()
expect(b).not.toBeNull()
expect(c).toBeNull() // Important!
expect(d).not.toBeNull()
expect(b).not.toBe(d)
})
test('ssr no brotli', async () => {
const makeReq = login => fetch(url('/ssr' + (login ? '?login' : '')))
.then(r => r.text())
.then(h => /encoding-\$(.*)\$/.exec(h))
.then(m => (m && m[1] ? m[1] : null))
const result = await makeReq()
expect(result).toBe('gzip, deflate')
})
})

167
types/index.d.ts vendored
View File

@ -1,35 +1,164 @@
import { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import Vue from 'vue'
import { ResponsePromise, Options, BeforeRequestHook, AfterResponseHook, HTTPError } from 'ky'
import './vuex'
interface NuxtAxiosInstance extends AxiosInstance {
$request<T = any>(config: AxiosRequestConfig): Promise<T>
$get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>
$delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>
$head<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>
$options<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>
$post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>
$put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>
$patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
setHeader(name: string, value?: string | false, scopes?: string | string[]): void
setToken(token: string | false, type?: string, scopes?: string | string[]): void
type JSONObject = { [key: string]: JSONValue };
interface JSONArray extends Array<JSONValue> { }
type JSONValue = string | number | boolean | null | JSONObject | JSONArray;
onRequest(callback: (config: AxiosRequestConfig) => void): void
onResponse<T = any>(callback: (response: AxiosResponse<T>) => void): void
onError(callback: (error: AxiosError) => void): void
onRequestError(callback: (error: AxiosError) => void): void
onResponseError(callback: (error: AxiosError) => void): void
interface OptionsWithoutBody extends Omit<Options, 'body'> {
method?: 'get' | 'head'
}
interface OptionsWithBody extends Options {
method?: 'post' | 'put' | 'delete'
}
interface NuxtHTTPInstance {
/**
* Fetches the `input` URL with the option `{method: 'get'}`.
*
* @param input - `Request` object, `URL` object, or URL string.
* @returns Promise with `Body` method added.
*/
get(input: Request | URL | string, options?: Omit<Options, 'body'>): ResponsePromise;
/**
* Fetches the `input` URL with the option `{method: 'post'}`.
*
* @param input - `Request` object, `URL` object, or URL string.
* @returns Promise with `Body` method added.
*/
post(input: Request | URL | string, options?: Options): ResponsePromise;
/**
* Fetches the `input` URL with the option `{method: 'put'}`.
*
* @param input - `Request` object, `URL` object, or URL string.
* @returns Promise with `Body` method added.
*/
put(input: Request | URL | string, options?: Options): ResponsePromise;
/**
* Fetches the `input` URL with the option `{method: 'patch'}`.
*
* @param input - `Request` object, `URL` object, or URL string.
* @returns Promise with `Body` method added.
*/
patch(input: Request | URL | string, options?: Options): ResponsePromise;
/**
* Fetches the `input` URL with the option `{method: 'head'}`.
*
* @param input - `Request` object, `URL` object, or URL string.
* @returns Promise with `Body` method added.
*/
head(input: Request | URL | string, options?: Omit<Options, 'body'>): ResponsePromise;
/**
* Fetches the `input` URL with the option `{method: 'delete'}`.
*
* @param input - `Request` object, `URL` object, or URL string.
* @returns Promise with `Body` method added.
*/
delete(input: Request | URL | string, options?: Options): ResponsePromise;
/**
* Fetches the `input` URL with the option `{method: 'get'}`.
*
* @param input - `Request` object, `URL` object, or URL string.
* @returns Promise that resolves to JSON parsed value.
*/
$get<T= JSONValue>(input: Request | URL | string, options?: Omit<Options, 'body'>): Promise<T>;
/**
* Fetches the `input` URL with the option `{method: 'post'}`.
*
* @param input - `Request` object, `URL` object, or URL string.
* @returns Promise that resolves to JSON parsed value.
*/
$post<T = JSONValue>(input: Request | URL | string, options?: Options): Promise<T>;
/**
* Fetches the `input` URL with the option `{method: 'put'}`.
*
* @param input - `Request` object, `URL` object, or URL string.
* @returns Promise that resolves to JSON parsed value.
*/
$put<T = JSONValue>(input: Request | URL | string, options?: Options): Promise<T>;
/**
* Fetches the `input` URL with the option `{method: 'patch'}`.
*
* @param input - `Request` object, `URL` object, or URL string.
* @returns Promise that resolves to JSON parsed value.
*/
$patch<T = JSONValue>(input: Request | URL | string, options?: Options): Promise<T>;
/**
* Fetches the `input` URL with the option `{method: 'head'}`.
*
* @param input - `Request` object, `URL` object, or URL string.
* @returns Promise that resolves to JSON parsed value.
*/
$head<T = JSONValue>(input: Request | URL | string, options?: Omit<Options, 'body'>): Promise<T>;
/**
* Fetches the `input` URL with the option `{method: 'delete'}`.
*
* @param input - `Request` object, `URL` object, or URL string.
* @returns Promise that resolves to JSON parsed value.
*/
$delete<T = JSONValue>(input: Request | URL | string, options?: Options): Promise<T>;
/**
* Set a header on all subsequent requests.
* @param name - Header name.
* @param value - Heade value.
*/
setHeader(name: string, value?: string | false): void
/**
* Set `Authorization` header on all subsequent requests.
* @param name - Header name.
* @param value - Heade value.
*/
setToken(token: string | false, type?: string): void
/**
* Set a hook on `beforeRequest` (Before request is sent)
*
* This hook enables you to globally modify the requests right before it is sent. It will make no further changes to the request after this. The hook function receives the normalized options as the first argument. You could, for example, modify `options.headers` here.
*/
onRequest(hook: BeforeRequestHook): void
/**
* Set a hook on `afterResponse` (After the response is received)
*
* This hook enables you to globally read and optionally modify the responses. The return value of the hook function will be used as the response object if it's an instance of [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
*/
onResponse(hook: AfterResponseHook): void
/**
* Set a hook on `onError` (When request failed)
*
* This hook enables you to globally handle request errors.
*/
onError(hook: (HTTPError) => void): void
}
declare module '@nuxt/vue-app' {
interface Context {
$axios: NuxtAxiosInstance
$http: NuxtHTTPInstance
}
}
declare module 'vue/types/vue' {
interface Vue {
$axios: NuxtAxiosInstance
$http: NuxtHTTPInstance
}
}

4
types/vuex.d.ts vendored
View File

@ -1,7 +1,7 @@
import { NuxtAxiosInstance } from '.'
import { NuxtHTTPInstance } from '.'
declare module 'vuex' {
interface Store<S> {
$axios: NuxtAxiosInstance,
$http: NuxtHTTPInstance,
}
}

2445
yarn.lock

File diff suppressed because it is too large Load Diff