Javascript 未捕获(承诺中)类型错误:无法读取属性';切片';未定义的

Javascript 未捕获(承诺中)类型错误:无法读取属性';切片';未定义的,javascript,npm,webpack,babeljs,Javascript,Npm,Webpack,Babeljs,我正在使用Javascript、NPM、Babel和Webpack构建一个名为:Forkify的示例配方应用程序,我在其中使用自定义API API URL:forkify API.herokuapp.com 搜索结果 返回特定查询的配方列表 路径: 示例URL: 获取 返回有关特定配方的详细信息 路径: 示例URL: 当我在命令行中使用commandnpm start run运行项目时,当我在搜索框中输入query pizza时,我得到以下错误(附图) 以下是代码文件: index.js

我正在使用Javascript、NPM、Babel和Webpack构建一个名为:Forkify的示例配方应用程序,我在其中使用自定义API
API URL:forkify API.herokuapp.com

搜索结果

返回特定查询的配方列表

路径:

示例URL:

获取

返回有关特定配方的详细信息

路径:

示例URL:

当我在命令行中使用commandnpm start run运行项目时,当我在搜索框中输入query pizza时,我得到以下错误(附图)

以下是代码文件:

index.js

/*
Global state of the app
- search object
- current recipe object
- shopping list object
- liked recipe
*/

import Search from "./models/Search";
import Recipe from "./models/Recipe";
import * as searchView from "./views/searchView";
import { elements, renderLoader, clearLoader } from "./views/base";
const state = {};

/* SEARCH CONTROLLER */

const controlSearch = async () => {
    // 1. Get query from the view.
    const query = searchView.getInput(); //TODO
    // console.log(query);
    if (query) {
        // 2. New search object and add it to state.
        state.search = new Search(query);

        // 3. Prepare UI for results.
        searchView.clearinput();
        searchView.clearResults();
        renderLoader(elements.searchRes);

        // 4. Search for recipes.
        await state.search.getResults();

        // 5. Render results on UI.
        clearLoader();
        searchView.renderResults(state.search.result);
    }
}

elements.searchForm.addEventListener("submit", e => {
    e.preventDefault();
    controlSearch();
});

elements.searchResPages.addEventListener("click",e=>{
    const btn=e.target.closest(".btn-inline");
    if (btn) {
        const goToPage=parseInt(btn.dataset.goto,10);
        searchView.clearResults();
        searchView.renderResults(state.search.result,goToPage);
    }
});

import axios from "axios";
// import {proxy} from "../config";
export default class Search{
    constructor(query){
        this.query=query;
    }

    async getResults() {
        try{
        const res = axios(`https://forkify-api.herokuapp.com/api/search?q=${this.query}`);
        this.result = res.data.recipes;
        // console.log(this.result);
        }
        catch(error){
            alert(error);
        }
    };

}
import { elements } from "./base";

export const getInput = () => elements.searchInput.value;

export const clearinput = () => {
    elements.searchInput.value = "";
};

export const clearResults = () => {
    elements.searchResList.innerHTML = "";
    elements.searchResPages.innerHTML = "";
};

/*
"pasta with tomato and spinach"
acc:0/acc+curr.length=5 /newTitle =['pasta']
acc:5/acc+curr.length=9 /newTitle =['pasta','with']
acc:9/acc+curr.length=15 /newTitle =['pasta','with','tomato']
acc:15/acc+curr.length=18 /newTitle =['pasta','with','tomato']
acc:18/acc+curr.length=25 /newTitle =['pasta','with','tomato']
*/

const limitRecipeTitle = (title, limit = 17) => {
    const newTitle = [];
    if (title.length > limit) {
        title.split(" ").reduce((acc, curr) => {
            if (acc + curr.length <= limit) {
                newTitle.push(curr);
            }
            return acc + curr.length;
        }, 0);
        // return the results
        return `${newTitle.join(' ')}...`;
    }
    return title;
};

const renderRecipe = recipe => {
    const markup = `
    <li>
        <a class="results__link" href="#${recipe.recipe_id}">
            <figure class="results__fig">
                <img src="${recipe.image_url}" alt="${recipe.title}">
            </figure>
            <div class="results__data">
                <h4 class="results__name">${limitRecipeTitle(recipe.title)}</h4>
                 <p class="results__author">${recipe.publisher}</p>
            </div>
        </a>
    </li>
    `;
    elements.searchResList.insertAdjacentHTML("beforeend", markup);
};

// type: "prev" or "next"
const createButton = (page, type) => `
<button class="btn-inline results__btn--${type}" data-goto=${type === "prev" ? page - 1 : page + 1}>
<span>Page ${ type === "prev" ? page - 1 : page + 1}</span>
<svg class="search__icon">
    <use href="img/icons.svg#icon-triangle-${ type === "prev" ? "left" : "right"}"></use>
</svg>
</button>
`


const renderButtons = (page, numResults, resPerPage) => {
    const pages = Math.ceil(numResults / resPerPage);
    let button;
    if (page === 1 && pages > 1) {
        // Only button to go to next page.
        button = createButton(page, "next");
    }
    else if (page < pages) {
        // Both buttons
        button = `
        ${createButton(page, "prev")}
        ${createButton(page, "next")}
        `;
    }
    else if (page === pages && pages > 1) {
        // Only button to go to previous page.
        button = createButton(page, "prev");
    }
    elements.searchResPages.insertAdjacentHTML("afterbegin", button);
}

export const renderResults = (recipes, page = 1, resPerPage = 10) => {
    // render results of current page
    const start = (page - 1) * resPerPage;
    const end = page * resPerPage;
    // recipes.slice(start,end).forEach(renderRecipe);

    recipes.slice(start, end).forEach(renderRecipe);

    // render pagination buttons
    renderButtons(page, recipes.length, resPerPage);
};
import axios from "axios";
// import {key} from "../config";
export default class Recipe{
    constructor (id){
        this.id=id;
    }

    async getRecipe(){
        try {
            const res=await axios(`https://forkify-api.herokuapp.com/api/get?rId=${this.query}`);
            console.log(res);
        } catch (error) {
            console.log(error);
        }
    }
};
export const elements = {
    searchForm: document.querySelector(".search"),
    searchInput: document.querySelector(".search__field"),
    searchRes: document.querySelector(".results"),
    searchResList: document.querySelector(".results__list"),
    searchResPages:document.querySelector(".results__pages")
};

export const elementStrings = {
    loader: "loader"
};

export const renderLoader = parent => {
    const loader = `
    <div class="${elementStrings.loader}">
        <svg>
            <use href="img/icons.svg#icon-cw">
            </use>
        </svg>
    </div>
    `;
    parent.insertAdjacentHTML("afterbegin", loader);
};

export const clearLoader = () => {
    const loader = document.querySelector(`.${elementStrings.loader}`);
    if (loader) loader.parentElement.removeChild(loader);
};
Search.js

/*
Global state of the app
- search object
- current recipe object
- shopping list object
- liked recipe
*/

import Search from "./models/Search";
import Recipe from "./models/Recipe";
import * as searchView from "./views/searchView";
import { elements, renderLoader, clearLoader } from "./views/base";
const state = {};

/* SEARCH CONTROLLER */

const controlSearch = async () => {
    // 1. Get query from the view.
    const query = searchView.getInput(); //TODO
    // console.log(query);
    if (query) {
        // 2. New search object and add it to state.
        state.search = new Search(query);

        // 3. Prepare UI for results.
        searchView.clearinput();
        searchView.clearResults();
        renderLoader(elements.searchRes);

        // 4. Search for recipes.
        await state.search.getResults();

        // 5. Render results on UI.
        clearLoader();
        searchView.renderResults(state.search.result);
    }
}

elements.searchForm.addEventListener("submit", e => {
    e.preventDefault();
    controlSearch();
});

elements.searchResPages.addEventListener("click",e=>{
    const btn=e.target.closest(".btn-inline");
    if (btn) {
        const goToPage=parseInt(btn.dataset.goto,10);
        searchView.clearResults();
        searchView.renderResults(state.search.result,goToPage);
    }
});

import axios from "axios";
// import {proxy} from "../config";
export default class Search{
    constructor(query){
        this.query=query;
    }

    async getResults() {
        try{
        const res = axios(`https://forkify-api.herokuapp.com/api/search?q=${this.query}`);
        this.result = res.data.recipes;
        // console.log(this.result);
        }
        catch(error){
            alert(error);
        }
    };

}
import { elements } from "./base";

export const getInput = () => elements.searchInput.value;

export const clearinput = () => {
    elements.searchInput.value = "";
};

export const clearResults = () => {
    elements.searchResList.innerHTML = "";
    elements.searchResPages.innerHTML = "";
};

/*
"pasta with tomato and spinach"
acc:0/acc+curr.length=5 /newTitle =['pasta']
acc:5/acc+curr.length=9 /newTitle =['pasta','with']
acc:9/acc+curr.length=15 /newTitle =['pasta','with','tomato']
acc:15/acc+curr.length=18 /newTitle =['pasta','with','tomato']
acc:18/acc+curr.length=25 /newTitle =['pasta','with','tomato']
*/

const limitRecipeTitle = (title, limit = 17) => {
    const newTitle = [];
    if (title.length > limit) {
        title.split(" ").reduce((acc, curr) => {
            if (acc + curr.length <= limit) {
                newTitle.push(curr);
            }
            return acc + curr.length;
        }, 0);
        // return the results
        return `${newTitle.join(' ')}...`;
    }
    return title;
};

const renderRecipe = recipe => {
    const markup = `
    <li>
        <a class="results__link" href="#${recipe.recipe_id}">
            <figure class="results__fig">
                <img src="${recipe.image_url}" alt="${recipe.title}">
            </figure>
            <div class="results__data">
                <h4 class="results__name">${limitRecipeTitle(recipe.title)}</h4>
                 <p class="results__author">${recipe.publisher}</p>
            </div>
        </a>
    </li>
    `;
    elements.searchResList.insertAdjacentHTML("beforeend", markup);
};

// type: "prev" or "next"
const createButton = (page, type) => `
<button class="btn-inline results__btn--${type}" data-goto=${type === "prev" ? page - 1 : page + 1}>
<span>Page ${ type === "prev" ? page - 1 : page + 1}</span>
<svg class="search__icon">
    <use href="img/icons.svg#icon-triangle-${ type === "prev" ? "left" : "right"}"></use>
</svg>
</button>
`


const renderButtons = (page, numResults, resPerPage) => {
    const pages = Math.ceil(numResults / resPerPage);
    let button;
    if (page === 1 && pages > 1) {
        // Only button to go to next page.
        button = createButton(page, "next");
    }
    else if (page < pages) {
        // Both buttons
        button = `
        ${createButton(page, "prev")}
        ${createButton(page, "next")}
        `;
    }
    else if (page === pages && pages > 1) {
        // Only button to go to previous page.
        button = createButton(page, "prev");
    }
    elements.searchResPages.insertAdjacentHTML("afterbegin", button);
}

export const renderResults = (recipes, page = 1, resPerPage = 10) => {
    // render results of current page
    const start = (page - 1) * resPerPage;
    const end = page * resPerPage;
    // recipes.slice(start,end).forEach(renderRecipe);

    recipes.slice(start, end).forEach(renderRecipe);

    // render pagination buttons
    renderButtons(page, recipes.length, resPerPage);
};
import axios from "axios";
// import {key} from "../config";
export default class Recipe{
    constructor (id){
        this.id=id;
    }

    async getRecipe(){
        try {
            const res=await axios(`https://forkify-api.herokuapp.com/api/get?rId=${this.query}`);
            console.log(res);
        } catch (error) {
            console.log(error);
        }
    }
};
export const elements = {
    searchForm: document.querySelector(".search"),
    searchInput: document.querySelector(".search__field"),
    searchRes: document.querySelector(".results"),
    searchResList: document.querySelector(".results__list"),
    searchResPages:document.querySelector(".results__pages")
};

export const elementStrings = {
    loader: "loader"
};

export const renderLoader = parent => {
    const loader = `
    <div class="${elementStrings.loader}">
        <svg>
            <use href="img/icons.svg#icon-cw">
            </use>
        </svg>
    </div>
    `;
    parent.insertAdjacentHTML("afterbegin", loader);
};

export const clearLoader = () => {
    const loader = document.querySelector(`.${elementStrings.loader}`);
    if (loader) loader.parentElement.removeChild(loader);
};
searchView.js

/*
Global state of the app
- search object
- current recipe object
- shopping list object
- liked recipe
*/

import Search from "./models/Search";
import Recipe from "./models/Recipe";
import * as searchView from "./views/searchView";
import { elements, renderLoader, clearLoader } from "./views/base";
const state = {};

/* SEARCH CONTROLLER */

const controlSearch = async () => {
    // 1. Get query from the view.
    const query = searchView.getInput(); //TODO
    // console.log(query);
    if (query) {
        // 2. New search object and add it to state.
        state.search = new Search(query);

        // 3. Prepare UI for results.
        searchView.clearinput();
        searchView.clearResults();
        renderLoader(elements.searchRes);

        // 4. Search for recipes.
        await state.search.getResults();

        // 5. Render results on UI.
        clearLoader();
        searchView.renderResults(state.search.result);
    }
}

elements.searchForm.addEventListener("submit", e => {
    e.preventDefault();
    controlSearch();
});

elements.searchResPages.addEventListener("click",e=>{
    const btn=e.target.closest(".btn-inline");
    if (btn) {
        const goToPage=parseInt(btn.dataset.goto,10);
        searchView.clearResults();
        searchView.renderResults(state.search.result,goToPage);
    }
});

import axios from "axios";
// import {proxy} from "../config";
export default class Search{
    constructor(query){
        this.query=query;
    }

    async getResults() {
        try{
        const res = axios(`https://forkify-api.herokuapp.com/api/search?q=${this.query}`);
        this.result = res.data.recipes;
        // console.log(this.result);
        }
        catch(error){
            alert(error);
        }
    };

}
import { elements } from "./base";

export const getInput = () => elements.searchInput.value;

export const clearinput = () => {
    elements.searchInput.value = "";
};

export const clearResults = () => {
    elements.searchResList.innerHTML = "";
    elements.searchResPages.innerHTML = "";
};

/*
"pasta with tomato and spinach"
acc:0/acc+curr.length=5 /newTitle =['pasta']
acc:5/acc+curr.length=9 /newTitle =['pasta','with']
acc:9/acc+curr.length=15 /newTitle =['pasta','with','tomato']
acc:15/acc+curr.length=18 /newTitle =['pasta','with','tomato']
acc:18/acc+curr.length=25 /newTitle =['pasta','with','tomato']
*/

const limitRecipeTitle = (title, limit = 17) => {
    const newTitle = [];
    if (title.length > limit) {
        title.split(" ").reduce((acc, curr) => {
            if (acc + curr.length <= limit) {
                newTitle.push(curr);
            }
            return acc + curr.length;
        }, 0);
        // return the results
        return `${newTitle.join(' ')}...`;
    }
    return title;
};

const renderRecipe = recipe => {
    const markup = `
    <li>
        <a class="results__link" href="#${recipe.recipe_id}">
            <figure class="results__fig">
                <img src="${recipe.image_url}" alt="${recipe.title}">
            </figure>
            <div class="results__data">
                <h4 class="results__name">${limitRecipeTitle(recipe.title)}</h4>
                 <p class="results__author">${recipe.publisher}</p>
            </div>
        </a>
    </li>
    `;
    elements.searchResList.insertAdjacentHTML("beforeend", markup);
};

// type: "prev" or "next"
const createButton = (page, type) => `
<button class="btn-inline results__btn--${type}" data-goto=${type === "prev" ? page - 1 : page + 1}>
<span>Page ${ type === "prev" ? page - 1 : page + 1}</span>
<svg class="search__icon">
    <use href="img/icons.svg#icon-triangle-${ type === "prev" ? "left" : "right"}"></use>
</svg>
</button>
`


const renderButtons = (page, numResults, resPerPage) => {
    const pages = Math.ceil(numResults / resPerPage);
    let button;
    if (page === 1 && pages > 1) {
        // Only button to go to next page.
        button = createButton(page, "next");
    }
    else if (page < pages) {
        // Both buttons
        button = `
        ${createButton(page, "prev")}
        ${createButton(page, "next")}
        `;
    }
    else if (page === pages && pages > 1) {
        // Only button to go to previous page.
        button = createButton(page, "prev");
    }
    elements.searchResPages.insertAdjacentHTML("afterbegin", button);
}

export const renderResults = (recipes, page = 1, resPerPage = 10) => {
    // render results of current page
    const start = (page - 1) * resPerPage;
    const end = page * resPerPage;
    // recipes.slice(start,end).forEach(renderRecipe);

    recipes.slice(start, end).forEach(renderRecipe);

    // render pagination buttons
    renderButtons(page, recipes.length, resPerPage);
};
import axios from "axios";
// import {key} from "../config";
export default class Recipe{
    constructor (id){
        this.id=id;
    }

    async getRecipe(){
        try {
            const res=await axios(`https://forkify-api.herokuapp.com/api/get?rId=${this.query}`);
            console.log(res);
        } catch (error) {
            console.log(error);
        }
    }
};
export const elements = {
    searchForm: document.querySelector(".search"),
    searchInput: document.querySelector(".search__field"),
    searchRes: document.querySelector(".results"),
    searchResList: document.querySelector(".results__list"),
    searchResPages:document.querySelector(".results__pages")
};

export const elementStrings = {
    loader: "loader"
};

export const renderLoader = parent => {
    const loader = `
    <div class="${elementStrings.loader}">
        <svg>
            <use href="img/icons.svg#icon-cw">
            </use>
        </svg>
    </div>
    `;
    parent.insertAdjacentHTML("afterbegin", loader);
};

export const clearLoader = () => {
    const loader = document.querySelector(`.${elementStrings.loader}`);
    if (loader) loader.parentElement.removeChild(loader);
};
base.js

/*
Global state of the app
- search object
- current recipe object
- shopping list object
- liked recipe
*/

import Search from "./models/Search";
import Recipe from "./models/Recipe";
import * as searchView from "./views/searchView";
import { elements, renderLoader, clearLoader } from "./views/base";
const state = {};

/* SEARCH CONTROLLER */

const controlSearch = async () => {
    // 1. Get query from the view.
    const query = searchView.getInput(); //TODO
    // console.log(query);
    if (query) {
        // 2. New search object and add it to state.
        state.search = new Search(query);

        // 3. Prepare UI for results.
        searchView.clearinput();
        searchView.clearResults();
        renderLoader(elements.searchRes);

        // 4. Search for recipes.
        await state.search.getResults();

        // 5. Render results on UI.
        clearLoader();
        searchView.renderResults(state.search.result);
    }
}

elements.searchForm.addEventListener("submit", e => {
    e.preventDefault();
    controlSearch();
});

elements.searchResPages.addEventListener("click",e=>{
    const btn=e.target.closest(".btn-inline");
    if (btn) {
        const goToPage=parseInt(btn.dataset.goto,10);
        searchView.clearResults();
        searchView.renderResults(state.search.result,goToPage);
    }
});

import axios from "axios";
// import {proxy} from "../config";
export default class Search{
    constructor(query){
        this.query=query;
    }

    async getResults() {
        try{
        const res = axios(`https://forkify-api.herokuapp.com/api/search?q=${this.query}`);
        this.result = res.data.recipes;
        // console.log(this.result);
        }
        catch(error){
            alert(error);
        }
    };

}
import { elements } from "./base";

export const getInput = () => elements.searchInput.value;

export const clearinput = () => {
    elements.searchInput.value = "";
};

export const clearResults = () => {
    elements.searchResList.innerHTML = "";
    elements.searchResPages.innerHTML = "";
};

/*
"pasta with tomato and spinach"
acc:0/acc+curr.length=5 /newTitle =['pasta']
acc:5/acc+curr.length=9 /newTitle =['pasta','with']
acc:9/acc+curr.length=15 /newTitle =['pasta','with','tomato']
acc:15/acc+curr.length=18 /newTitle =['pasta','with','tomato']
acc:18/acc+curr.length=25 /newTitle =['pasta','with','tomato']
*/

const limitRecipeTitle = (title, limit = 17) => {
    const newTitle = [];
    if (title.length > limit) {
        title.split(" ").reduce((acc, curr) => {
            if (acc + curr.length <= limit) {
                newTitle.push(curr);
            }
            return acc + curr.length;
        }, 0);
        // return the results
        return `${newTitle.join(' ')}...`;
    }
    return title;
};

const renderRecipe = recipe => {
    const markup = `
    <li>
        <a class="results__link" href="#${recipe.recipe_id}">
            <figure class="results__fig">
                <img src="${recipe.image_url}" alt="${recipe.title}">
            </figure>
            <div class="results__data">
                <h4 class="results__name">${limitRecipeTitle(recipe.title)}</h4>
                 <p class="results__author">${recipe.publisher}</p>
            </div>
        </a>
    </li>
    `;
    elements.searchResList.insertAdjacentHTML("beforeend", markup);
};

// type: "prev" or "next"
const createButton = (page, type) => `
<button class="btn-inline results__btn--${type}" data-goto=${type === "prev" ? page - 1 : page + 1}>
<span>Page ${ type === "prev" ? page - 1 : page + 1}</span>
<svg class="search__icon">
    <use href="img/icons.svg#icon-triangle-${ type === "prev" ? "left" : "right"}"></use>
</svg>
</button>
`


const renderButtons = (page, numResults, resPerPage) => {
    const pages = Math.ceil(numResults / resPerPage);
    let button;
    if (page === 1 && pages > 1) {
        // Only button to go to next page.
        button = createButton(page, "next");
    }
    else if (page < pages) {
        // Both buttons
        button = `
        ${createButton(page, "prev")}
        ${createButton(page, "next")}
        `;
    }
    else if (page === pages && pages > 1) {
        // Only button to go to previous page.
        button = createButton(page, "prev");
    }
    elements.searchResPages.insertAdjacentHTML("afterbegin", button);
}

export const renderResults = (recipes, page = 1, resPerPage = 10) => {
    // render results of current page
    const start = (page - 1) * resPerPage;
    const end = page * resPerPage;
    // recipes.slice(start,end).forEach(renderRecipe);

    recipes.slice(start, end).forEach(renderRecipe);

    // render pagination buttons
    renderButtons(page, recipes.length, resPerPage);
};
import axios from "axios";
// import {key} from "../config";
export default class Recipe{
    constructor (id){
        this.id=id;
    }

    async getRecipe(){
        try {
            const res=await axios(`https://forkify-api.herokuapp.com/api/get?rId=${this.query}`);
            console.log(res);
        } catch (error) {
            console.log(error);
        }
    }
};
export const elements = {
    searchForm: document.querySelector(".search"),
    searchInput: document.querySelector(".search__field"),
    searchRes: document.querySelector(".results"),
    searchResList: document.querySelector(".results__list"),
    searchResPages:document.querySelector(".results__pages")
};

export const elementStrings = {
    loader: "loader"
};

export const renderLoader = parent => {
    const loader = `
    <div class="${elementStrings.loader}">
        <svg>
            <use href="img/icons.svg#icon-cw">
            </use>
        </svg>
    </div>
    `;
    parent.insertAdjacentHTML("afterbegin", loader);
};

export const clearLoader = () => {
    const loader = document.querySelector(`.${elementStrings.loader}`);
    if (loader) loader.parentElement.removeChild(loader);
};
导出常量元素={
searchForm:document.querySelector(“.search”),
searchInput:document.querySelector(“.search\u字段”),
searchRes:document.querySelector(“.results”),
searchResList:document.querySelector(“结果列表”),
searchResPages:document.querySelector(“.results\uuuu pages”)
};
导出常量元素字符串={
加载器:“加载器”
};
导出常量renderLoader=parent=>{
常量加载器=`
`;
parent.insertAdjacentHTML(“afterbegin”,loader);
};
导出常量clearLoader=()=>{
const loader=document.querySelector(`.${elementStrings.loader}`);
if(loader)loader.parentElement.removeChild(loader);
};

有什么解决方案吗?

您的
axios
调用Search.js时缺少
wait
关键字:

const res = await axios(`https://forkify-api.herokuapp.com/api/search?q=${this.query}`);
this.result = res.data.recipes;
这就是您看到错误的原因:

TypeError:无法读取未定义的属性“recipes”

然后,
undefined
被传递到
renderResults
,这就是为什么您会看到控制台错误:

TypeError:无法读取未定义的属性“slice”

您可以将
recipes
参数默认为空数组,以确保即使传递了
undefined
也可以调用
slice

export const renderResults = (recipes = [], page = 1, resPerPage = 10) => {
  // ...
}

把它上传到codesandobx或者类似的东西会更容易解决问题史蒂夫·霍尔加多:非常感谢,它成功了。。。。我被这个问题难住了。。三个多小时。再次感谢……没问题——很乐意帮忙:)