App Router и Pages Router: что изменилось в Next.js

Привет, Хабр!

Как могут заметить разработчики, фреймворк Next очень активно развивается. Так, некоторое время назад в 13 версии появилась новая парадигма (модель) для создания приложений — app router, которая должна прийти на смену старой pages router.

В этой статье мы постараемся наглядно продемонстрировать и рассказать, что же поменялось в работе приложения с появлением app router, какие изменения произошли в сравнении с pages router, что нового успели добавить разработчики, а от чего они отказались.

Изменение структуры проекта

Начнем со структуры самого проекта. В проекте с pages-роутингом в папке src у нас
была папка pages, в которой находились все страницы нашего приложения. В ней лежала
главная страница index и при необходимости добавления новых, нужно было создать одноименную папку и файл index внутри. Стили по дефолту, как и фавиконка, лежали в отдельных папках.

522c125bc0d0dc2afa4d9eb85f208316.png

В app-роутинге убрали папку pages, теперь главная страница поменяла свое название с index на page и лежит в папке app рядом со стилями, фавиконкой и layout, речь о котором пойдет позже.

1441f137490cc48a5f51b2b159a90aca.pngbdad6c813108aeb6f764cf2fee178539.png

Для добавления же новой страницы, прямо в папке app мы так же должны создать одноименную будущему роутингу папку, внутри которой будет находиться файл с нашей страницей — page.

При это важно отметить, что если в папке src мы создадим папку pages (по аналогии с работой старого роутера) и создадим там новый файл (например, test.tsx), то у нас будет создана страница с роутом /test. Таким образом, в одном приложении мы можем использовать как новый, так и старый подходы.

ab22591d2a690929f697fee6c3f1d1da.png

Работа с Метаданными

Метаданные помогают нам при работе с SEO и поисковой оптимизации нашего веб-приложения, а также служат инструментом добавления дополнительной информации, стилей, изображений и прочего.

В pages-роутинге работа с метаданными была реализована через тег . Внутри него мы задаем теги с нужными нам параметрами, например, </strong>или<strong> <link></strong>. Существует так же тег <strong><meta> </strong>для работы, например, с <em>description</em> или <em>open graph</em>.Тег <strong><Head> </strong>можно задать на каждой странице приложения.</p> <p>В app-роутинге задать метаданные мы можем с помощью <em>metadata</em> (объект или функция), которую мы должны экспортировать. Тип у константы будет <strong>Metadata</strong>, который включает в себя такие параметры как <strong>metadataBase, title, description, authors, generator, keywords </strong>и так далее. Метаданные мы можем задать как в файле <em>layout</em>, так и в файле нашей странички.</p> <p><img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/ac2/8d9/529/ac28d952955da7a7a98235c87798bc57.png" alt="ac28d952955da7a7a98235c87798bc57.png" /></p> <p><strong>Layout, _app и _document</strong></p> <p>Когда мы разрабатываем приложение, то нередко сталкиваемся с тем, что некоторые компоненты повторяются у нас на каждой (или почти каждой) странице. Например, хедер, футер, панель навигации. Для того, чтобы заново не отрисовывать компоненты каждый раз при переходе с одной страницы на другую, повысить производительность приложения и сохранить состояния, такие компоненты выносят отдельно в компонент <em>Layout</em>. Он является оберткой для всего нашего приложения или его части.  В качестве пропcов он принимает children — ту часть страницы, которая будет изменяться, в отличие от хедера, футера и других компонентов.</p> <p>В pages-роутинге при необходимости воспользоваться <em>Layout</em> нам приходилось писать его самим, так как в Next не было его при развертывании проекта. В pages router в папке <em>pages</em> у нас находились зарезервированные файлы _<em>app</em> и _<em>document</em>. Файл <em>app</em> был необходим для инициализации страниц приложения, а <em>_document</em> обновлял теги <strong>html bod<em>y</em></strong> и рендерился на сервере. Теперь функционал обоих этих файлов перешел в новый файл <em>layout</em>, о котором сейчас и пойдет речь.  </p> <p>В app-роутинге у нас есть встроенный файл <em>layout</em>. <em>RootLayout </em>представляет собой компонент, принимающий <em>сhildren</em> в качестве пропсов и состоящий из <em>body</em>, куда и выводит <em>children</em>. Если же для какого-то роута нам нужен отдельный <em>layout</em>, то нам следует добавить в папку страницы уже новый файл <em>layout</em>, тем самым переопределив его.</p> <p><img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/079/466/f5d/079466f5d019e1f351649b531d5fd773.png" alt="079466f5d019e1f351649b531d5fd773.png" /></p> <p><strong>Template</strong></p> <p>Вслед за <em>layout</em> хочется рассказать об очень схожей возможности оборачивать страницы, которая называется <em>Template</em>. Структура <em>template</em> (дословно «шаблон») очень сильно схожа с <em>layout</em>, но с одним существенным отличием: при переходе на новую страницу шаблон создает новый экземпляр для каждого дочернего элемента, соответственно, происходит сбрасывание состояний и эффектов.</p> <p><em>Template</em> можно создать так же, как и <em>layout, </em> просто добавив одноименный файл, остальная логика отличий не имеет. Шаблоны могут быть полезны в случае, если внутри приложения нужна изоляция, нет необходимости сохранять состояние или нужно, чтобы эффекты отрабатывали при каждой смене роута. Шаблоном можно воспользоваться, например, при работе со статистикой или отслеживании просмотров страницы.</p> <p><img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/34f/055/75c/34f05575c48488136a6ef9e2200eb6b6.png" alt="34f05575c48488136a6ef9e2200eb6b6.png" /></p> <p><strong>Группы роутов</strong></p> <p>Для улучшения файловой структуры нашего проекта в Next была добавлена возможность группировать роуты. Группы роутов не влияют на пути в нашем приложении, но позволяют добавлять несколько корневых <em>layout</em>, а также упрощать разработку, изолируя одну часть проекта от другой.</p> <p>Допустим, возникает необходимость изолировать покупку, авторизацию или любую другую логику от всего остального приложения. В таком случае мы можем сделать группировку роутов. Для этого нужно создать папку с именем, обернув его в круглые скобки, после чего перенести наши страницы в нужную папку. Там мы можем задать для группы наших роутов свои отдельные <em>layout</em> и <em>template</em> файлы, которые не будут пересекаться с другими страницами и группами роутов.</p> <p><img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/efd/81d/75d/efd81d75d9322af65829389e762c045a.png" alt="efd81d75d9322af65829389e762c045a.png" /></p> <p><strong>Параллельные роуты</strong></p> <p>Для оптимизации нашего приложения, Next также добавил возможность параллельного роутинга в приложении. Мы можем указать кусочки нашей странички, которые могут грузиться параллельно.</p> <p>Параллельная маршрутизация позволяет одновременно или выборочно отображать одну или несколько страниц в одном макете <em>(Layout).</em></p> <p><img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/b12/ab5/4bf/b12ab54bffc60778ae911a92e3595082.png" alt="b12ab54bffc60778ae911a92e3595082.png" /></p> <p>При этом у нас есть возможность отрисовки резервного варианта с помощью файла <em>default.</em> Во время «жесткой» навигации Next не может определить активное состояние слотов, поэтому отобразит содержание файла <em>default</em> или 404, если файла <em>default</em> нет. </p> <p><img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/450/945/7bc/4509457bc5a35585c2775f8e29b4b1c1.png" alt="4509457bc5a35585c2775f8e29b4b1c1.png" /></p> <p>В app router также была добавлена возможность работы с перехватывающими роутами. Их суть заключается в том, чтобы предоставлять альтернативное поведение приложения при смене роутинга с определенной страницы или группы роутов.</p> <p>Самый простой и распространенный вариант — это работа с каталогом фото или других постов, при клике на который у нас должно открываться модальное окно на странице каталога. Но при перезагрузке мы должны попадать на отдельную страницу с фото. Роут при этом изменяться не должен.</p> <p>При переходе со страницы <em>/feed </em>на <em>/photo/id</em> роутер будет перехватываться, url меняться, но мы будем оставаться на той же странице. При перезагрузке страницы <em>/photo/id</em>, роут перехватываться не будет, и мы будем попадать на обычную страницу.</p> <p><img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/f58/cc1/38e/f58cc138e9180e300d9d6913de51b5c3.png" alt="f58cc138e9180e300d9d6913de51b5c3.png" /></p> <p>Количество точек перед названием роута, который нужно будет перезаписать, имеет свое значение: </p> <p>•     (.) — для сегментов на одном уровне (см. пример выше)</p> <p>•     (…) — для сегментов на уровне выше (см. картинку. Изначально страница <em>photo</em> находится на одном уровне с <em>feed</em>, поэтому страница <em>photo</em> внутри папки <em>feed</em> имеет две точки)</p> <p>•     (…)(…) — для сегментов на два уровня выше</p> <p>• (…) — для сегментов из корня app директории </p> <p><img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/7d8/dec/6f1/7d8dec6f10416108580c8db8adeab699.png" alt="7d8dec6f10416108580c8db8adeab699.png" /></p> <p><strong>Loading, Error, Not Found</strong></p> <p>В Next были добавлена возможность обработки загрузки и ошибки, соответственно <em>loading</em> и <em>error</em> файлов.</p> <p>Компонент <em>Loading</em> упрощает нам работу при взаимодействии с асинхронным кодом. Он автоматически оборачивает страницу в <em>React Suspense</em>. Компонент <em>Loading</em> будет показан сразу же при первой загрузке. Важно отметить, что он будет работать только для серверных компонентов, но не для клиентских (о них мы поговорим позже).</p> <p><img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/096/99c/8bf/09699c8bf596d5fef0a8b820d05e32cc.png" alt="09699c8bf596d5fef0a8b820d05e32cc.png" /></p> <p>Компонент <em>Error</em> позволяет нам обрабатывать ошибки на странице нашего приложения. <em>Error </em>автоматически оборачивает страницу и <em>layout</em> (если он есть) в <em>React error boundary</em>. Как и <em>layout, error</em> может быть у каждой страницы или группы роутов свой. <em>Error</em> принимает несколько аргументов: саму ошибку error и второй аргумент, функцию <em>reset</em>, которая должна заново отрисовать наш компонент с ошибкой. Как и в случае с <em>Loading</em>, компонент <em>Error</em> будет отрабатывать только при ошибке в серверном компоненте, но сам компонент будет являться клиентским, поэтому нужно будет добавить <em>«use client»</em>.</p> <p><strong>Работа с SSR, SSG, ISR и серверными компонентами</strong></p> <p>Посмотрим на то, как реализован <em>SSR (Server side rendering</em>), <em>SSG</em> <em>(Static site generation</em>) и <em>ISR</em> <em>(Incremental Static Regeneration)</em> в app router и pages router.</p> <p>Для работы с <em>SSR</em> в pages router раньше мы экспортировали асинхронную функцию <em>getServerSideProps</em>. Объект <em>props</em>, который мы возвращаем из этой функции, будет передан в качестве пропсов в наш компонент на сервере. Функция будет исполняться только на сервере, а перерендер будет происходить для каждого запроса. Для работы со статической генерацией <em>(SSG)</em> pages-router предлагает нам воспользоваться <em>getStaticProps</em> или <em>getStaticPaths</em> (для определения динамических роутов). И, наконец, для инкрементальной статической регенерации (<em>ISR</em>, где мы кэшируем данные, но ревалидируем их через определенные интервалы времени), мы пользовались тем же <em>getStaticProps </em>с дополнительным параметром <em>revalidate</em>.</p> <p>В отличие от pages router, где у нас есть <em>getServerSideProps</em>, в app router есть серверные компоненты, при этом по умолчанию используется статическая генерация <em>(SSG)</em>. То есть если мы создадим даже самый простой компонент с выводом текста в теге <strong><div></strong> и захотим вывести в логе какое-нибудь число, то в консоле браузера при загрузке страницы будет пусто, потому что все это будет происходить на стороне сервера. </p> <p>У серверных компонентов нет состояний, мы не можем использовать тот же <em>useState</em>.</p> <p>Для получения данных мы создаем асинхронную функцию с использованием <em>fetch</em>. Важно понимать, что это не дефолтный <em>fetch</em>, а <em>fetch</em> от Next (пропатченный). В качестве аргументов он принимает <em>url</em> и дополнительные параметры (method, body и тд). В нем можно так же задать кэширование данных, у которого будет несколько вариантов значений. В зависимости от значения, мы можем выбрать, как будет генерироваться наша страница.</p> <p>Значение <em>cache: «no-store»</em> эквивалентно рендерингу с помощью <em>SSR</em>, значение <em>cache</em> <em>«force-cache»</em> будет эквивалентно статической генерации (<em>SSG</em>).</p> <p><img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/dc9/67f/b79/dc967fb79d8e42e1dda0a28b9efd58df.png" alt="dc967fb79d8e42e1dda0a28b9efd58df.png" /></p> <p>Если же мы зададим значение с параметром <em>revalidate</em>, то это будет эквивалентно <em>ISR</em> — статической генерации с опцией ревалидации, где в качестве значения мы указывали количество секунд, через которое должна произойти ревалидация.</p> <p>При этом наши страницы и компоненты могут быть асинхронными. Мы можем прописать <em>async, </em> и уже в самом компоненте сделать обычный запрос, например через библиотеку axios. И если раньше для этого нам нужно было бы пользоваться <em>useEffec</em>t, писать асинхронную функцию, сохранять данные в стейт, то сейчас мы можем просто сделать запрос. </p> <p>Как было написано выше, в случае длительного ожидания ответа, на клиенте автоматически будет показан компонент из <em>loading</em> файла.</p> <p>Если при работе с запросами, вы не хотите использовать <em>fetch</em>, а предпочитаете воспользоваться сторонней библиотекой, сохраняя при этом возможность выбора генерации страницы с помощью <em>SSR, ISR</em> или <em>SSG</em>, то для этого в файле можно экспортировать зарезервированную константу <em>revalidate</em> или <em>dynamic</em>.</p> <p><img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/a6b/bf1/f9e/a6bbf1f9e51f6b4ae3b6db28bbb8dce3.png" alt="a6bbf1f9e51f6b4ae3b6db28bbb8dce3.png" /><img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/27a/fb0/590/27afb05909e2bf2efc13cfa2aa239368.png" alt="27afb05909e2bf2efc13cfa2aa239368.png" /></p> <p><strong>Клиентские компоненты</strong></p> <p>Добавить в наш компонент интерактив или какое-то состояние, работать с <em>браузерными API</em> в pages router мы могли по умолчанию. В app router мы должны воспользоваться специальными клиентскими компонентами (как уже было сказано выше, по умолчанию все компоненты у нас серверные). Обозначить их можно с помощью <em>«use client»</em>. К клиентским компонентам будут относиться кнопки, инпуты, компоненты, где нужно будет добавить анимацию, инфинити скролл и прочее.</p> <p><strong>Заключение</strong></p> <p>На этом статья подошла к концу, в ней мы описали лишь некоторую часть тех изменений, которые произошли в обновленном <em>app роутере</em> в Next.js. Как мы видим, часть функционала поменялась, были изменены подходы в работе с <em>SSR</em>, роутами, компонентами и прочим, добавлены новые возможности и исключены или перенесены некоторые инструменты предыдущих версий.</p> <p>Стоит отметить, что app router все еще находится на стадии разработки, хотя на сайте Next уже написано о рекомендации создавать новые приложения именно с помощью app router и постепенно внедрять его в текущие проекты. Нас ждет еще не несколько нововведений и последующих доработок. Тем не менее, Next сохраняет свою концепцию, позволяя удобно создавать приложения с поддержкой <em>SSR, SSG, ISR</em>.</p> <p>Благодарим за внимание! </p> <p class="copyrights"><span class="source">© <a target="_blank" rel="nofollow" href="https://habr.com/ru/companies/ppr/articles/792270/?utm_source=habrahabr&amp;utm_medium=rss&amp;utm_campaign=792270">Habrahabr.ru</a></span></p> </div> <br> <!--<div align="left"> <script type="text/topadvert"> load_event: page_load feed_id: 12105 pattern_id: 8187 tech_model: </script><script type="text/javascript" charset="utf-8" defer="defer" async="async" src="//loader.topadvert.ru/load.js"></script> </div> <br>--> <div style="padding-left: 20px;"> <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-2514821055276660" crossorigin="anonymous"></script> <!-- PCNews 336x280 --> <ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-2514821055276660" data-ad-slot="1200562049" data-ad-format="auto"></ins> <script> (adsbygoogle = window.adsbygoogle || []).push({}); </script> </div> <!-- comments --> <noindex> <div style="margin: 25px;" id="disqus_thread"></div> <script type="text/javascript"> var disqus_shortname = 'pcnewsru'; var disqus_identifier = '1382905'; var disqus_title = 'App Router и Pages Router: что изменилось в Next.js'; var disqus_url = 'http://pcnews.ru/blogs/app_router_i_pages_router_cto_izmenilos_v_nextjs-1382905.html'; (function() { var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true; dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js'; (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq); })(); </script> <!--<noscript>Please enable JavaScript to view the <a rel="nofollow" href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>--> <!--<a href="http://disqus.com" rel="nofollow" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>--> </noindex> </div> <br class="clearer"/> </div> <br class="clearer"/> <div id="footer-2nd"></div> <div id="footer"> <br/><br/> <ul class="horz-menu"> <li class="about"><a href="/info/about.html" title="О проекте">О проекте</a></li> <li class="additional-menu"><a href="/archive.html" title="Архив материалов">Архив</a> </li> <li class="additional-menu"><a href="/info/reklama.html" title="Реклама" class="menu-item"><strong>Реклама</strong></a> <a href="/info/partners.html" title="Партнёры" class="menu-item">Партнёры</a> <a href="/info/legal.html" title="Правовая информация" class="menu-item">Правовая информация</a> <a href="/info/contacts.html" title="Контакты" class="menu-item">Контакты</a> <a href="/feedback.html" title="Обратная связь" class="menu-item">Обратная связь</a></li> <li class="email"><a href="mailto:pcnews@pcnews.ru" title="Пишите нам на pcnews@pcnews.ru"><img src="/media/i/email.gif" alt="e-mail"/></a></li> <li style="visibility: hidden"> <noindex> <!-- Rating@Mail.ru counter --> <script type="text/javascript"> var _tmr = window._tmr || (window._tmr = []); _tmr.push({id: "93125", type: "pageView", start: (new Date()).getTime()}); (function (d, w, id) { if (d.getElementById(id)) return; var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id; ts.src = (d.location.protocol == "https:" ? "https:" : "http:") + "//top-fwz1.mail.ru/js/code.js"; var f = function () { var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s); }; if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); } })(document, window, "topmailru-code"); </script> <noscript> <div style="position:absolute;left:-10000px;"> <img src="//top-fwz1.mail.ru/counter?id=93125;js=na" style="border:0;" height="1" width="1" alt="Рейтинг@Mail.ru"/> </div> </noscript> <!-- //Rating@Mail.ru counter --> </noindex> </li> </ul> </div> <!--[if lte IE 7]> <iframe id="popup-iframe" frameborder="0" scrolling="no"></iframe> <![endif]--> <!--<div id="robot-image"><img class="rbimg" src="i/robot-img.png" alt="" width="182" height="305" /></div>--> <!--[if IE 6]> <script>DD_belatedPNG.fix('#robot-image, .rbimg');</script><![endif]--> </div> <!--[if lte IE 7]> <iframe id="ie-popup-iframe" frameborder="0" scrolling="no"></iframe> <![endif]--> <div id="footer-adlinks"></div> <noindex> <!--LiveInternet counter--><script type="text/javascript"> document.write("<a rel='nofollow' href='//www.liveinternet.ru/click' "+ "target=_blank><img src='//counter.yadro.ru/hit?t45.6;r"+ escape(document.referrer)+((typeof(screen)=="undefined")?"": ";s"+screen.width+"*"+screen.height+"*"+(screen.colorDepth? screen.colorDepth:screen.pixelDepth))+";u"+escape(document.URL)+ ";"+Math.random()+ "' alt='' title='LiveInternet' "+ "border='0' width='1' height='1'><\/a>") </script><!--/LiveInternet--> <!-- Rating@Mail.ru counter --> <script type="text/javascript"> var _tmr = window._tmr || (window._tmr = []); _tmr.push({id: "93125", type: "pageView", start: (new Date()).getTime()}); (function (d, w, id) { if (d.getElementById(id)) return; var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id; ts.src = "https://top-fwz1.mail.ru/js/code.js"; var f = function () {var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s);}; if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); } })(document, window, "topmailru-code"); </script><noscript><div> <img src="https://top-fwz1.mail.ru/counter?id=93125;js=na" style="border:0;position:absolute;left:-9999px;" alt="Top.Mail.Ru" /> </div></noscript> <!-- //Rating@Mail.ru counter --> <!-- Yandex.Metrika counter --> <script type="text/javascript"> (function (d, w, c) { (w[c] = w[c] || []).push(function () { try { w.yaCounter23235610 = new Ya.Metrika({ id: 23235610, clickmap: true, trackLinks: true, accurateTrackBounce: true, webvisor: true, trackHash: true }); } catch (e) { } }); var n = d.getElementsByTagName("script")[0], s = d.createElement("script"), f = function () { n.parentNode.insertBefore(s, n); }; s.type = "text/javascript"; s.async = true; s.src = "https://mc.yandex.ru/metrika/watch.js"; if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); } })(document, window, "yandex_metrika_callbacks"); </script> <noscript> <div><img src="https://mc.yandex.ru/watch/23235610" style="position:absolute; left:-9999px;" alt=""/> </div> </noscript> <!-- /Yandex.Metrika counter --> <!-- Default Statcounter code for PCNews.ru http://pcnews.ru--> <script type="text/javascript"> var sc_project=9446204; var sc_invisible=1; var sc_security="14d6509a"; </script> <script type="text/javascript" src="https://www.statcounter.com/counter/counter.js" async></script> <!-- End of Statcounter Code --> <script> (function (i, s, o, g, r, a, m) { i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () { (i[r].q = i[r].q || []).push(arguments) }, i[r].l = 1 * new Date(); a = s.createElement(o), m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m) })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); ga('create', 'UA-46280051-1', 'pcnews.ru'); ga('send', 'pageview'); </script> <script async="async" src="/assets/uptolike.js?pid=49295"></script> </noindex> <!--<div id="AdwolfBanner40x200_842695" ></div>--> <!--AdWolf Asynchronous Code Start --> <script type="text/javascript" src="https://pcnews.ru/js/blockAdblock.js"></script> <script type="text/javascript" src="/assets/jquery.min.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/jquery/jquery.json.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/jquery/jquery.form.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/jquery/jquery.easing.1.2.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/jquery/effects.core.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/browser-sniff.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/scripts.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-utils.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-auth.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-fiximg.js"></script> <script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-infobox.js"></script> </body> </html>