介绍
你好,你好吗?我是 Vítor,带着一个新项目回来了,可以帮助您提高编程技能。自从我上次发布教程以来已经有一段时间了。在过去的几个月里,我花了一些时间休息并专注于其他活动。在此期间,我开发了一个小型网络项目:博客,它成为本教程的重点。
在本指南中,我们将创建能够渲染 Markdown 的博客页面的前端。该应用程序将包括公共和私人路由、用户身份验证以及编写 Markdown 文本、添加照片、显示文章等功能。
随意定制您的应用程序,无论您喜欢什么——我什至鼓励这样做。
您可以在此处访问此应用程序的存储库:
冈德拉克08
/
博客平台
使用 Next.js/typescript 制作的博客平台。
博客平台
- 文本教程
成分
- next-auth - Next.js 的autenticação 图书馆
- github.com/markdown-it/markdown-it - markdown biblioteca。
- github.com/sindresorhus/github-markdown-css- Para dar estilo ao nosso markdown 编辑器。
- github.com/remarkjs/react-markdown - Biblioteca para renderizar markdown em nosso 组件react。
- github.com/remarkjs/remark-react/tree/4722bdf - React 中 Markdown 转换插件。
- codemirror.net - 网络编辑器组件。
- react-icons - 反应图标库。
科莫美国
npm i npm run start
服务器
você pode encontrar o server dessa aplicação em server
本教程还包括本指南中将使用的 Node.js 服务器的编写:
希望您喜欢。
编码愉快!
图书馆
以下是此项目中使用的库的摘要:
- next-auth - Next.js 的身份验证库
- github.com/markdown-it/markdown-it - Markdown 库。
- github.com/sindresorhus/github-markdown-css - 用于设计我们的 Markdown 编辑器。
- github.com/remarkjs/react-markdown - 用于在 React 组件中渲染 Markdown 的库。
- github.com/remarkjs/remark-react/tree/4722bdf - 将 Markdown 转换为 React 的插件。
- codemirror.net - Web 组件编辑器。
- react-icons - React 的图标库。
创建 React 项目
我们将使用最新版本的 Next.js 框架,在编写本教程时,版本为 13.4。
运行以下命令来创建项目:
npm i npm run start
安装过程中,选择模板设置。在本教程中,我将使用 TypeScript 作为编程语言,并使用 Tailwind CSS 框架来设计我们的应用程序。
配置
现在让我们安装我们将使用的所有库。
降价
npx create-next-app myblog
反应备注
npm i markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
代码镜像
remark remark-gfm remark-react
图标
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
然后通过删除我们不会使用的所有内容来清理安装的初始结构。
建筑学
这是我们应用程序的最终结构。
npm i react-icons @types/react-icons
第一步
配置next.config
在项目根目录的 next.config.js 文件中,让我们配置用于访问文章图像的域地址。对于本教程,或者如果您使用本地服务器,我们将使用 localhost。
确保包含此配置以确保在应用程序中正确加载图像。
src- |- app/ | |-(pages)/ | | |- (private)/ | | | |- (home) | | | |- editArticle/[id] | | | | | | | |- newArticle | | | - (public)/ | | | - article/[id] | | | - login | | | api/ | |- auth/[...nextAuth]/route.ts | |- global.css | |- layout.tsx | | - components/ | - context/ | - interfaces/ | - lib/ | - services/ middleware.ts
配置中间件
在应用程序 src/ 的根文件夹中,创建一个 middleware.ts 以验证对私有路由的访问。
const nextConfig = { images: { domains: ["localhost"], }, };
要了解有关中间件以及可以使用它们执行的所有操作的更多信息,请查看文档。
配置认证路由
在 /app 文件夹内,在 api/auth/[...nextauth] 中创建一个名为 Route.ts 的文件。它将包含我们的路由配置,使用 CredentialsProvider 连接到我们的身份验证 API。
CredentialsProvider 允许您处理使用任意凭据的登录,例如用户名和密码、域、双因素身份验证、硬件设备等。
首先,在项目的根目录中,创建一个 .env.local 文件并添加一个令牌,该令牌将用作我们的秘密。
npm i npm run start
接下来,让我们编写我们的身份验证系统,这个 NEXTAUTH_SECRET 将被添加到 src/app/auth/[...nextauth]/routes.ts 文件中的秘密中。
npx create-next-app myblog
认证提供者
让我们创建一个身份验证提供程序,一个上下文,它将在我们的私有路由的页面上共享用户的数据。稍后我们将使用它来包装我们的layout.tsx 文件之一。
在 src/context/auth-provider.tsx 中创建一个包含以下内容的文件:
npm i markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
全球风格
总的来说,在我们的应用程序中,我们将使用 Tailwind CSS 来创建我们的样式。但是,在某些地方,我们将在页面和组件之间共享自定义 CSS 类。
remark remark-gfm remark-react
布局
现在让我们编写私有和公共的布局。
应用程序/布局.tsx
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
页面/layout.tsx
npm i react-icons @types/react-icons
API调用
我们的应用程序将多次调用我们的 API,您可以调整此应用程序以使用任何外部 API。在我们的示例中,我们使用本地应用程序。如果你还没有看过后端教程和服务器创建,请查看。
在 src/services/ 中,我们编写以下函数:
- authService.ts:负责在服务器上验证用户身份的函数。
src- |- app/ | |-(pages)/ | | |- (private)/ | | | |- (home) | | | |- editArticle/[id] | | | | | | | |- newArticle | | | - (public)/ | | | - article/[id] | | | - login | | | api/ | |- auth/[...nextAuth]/route.ts | |- global.css | |- layout.tsx | | - components/ | - context/ | - interfaces/ | - lib/ | - services/ middleware.ts
2.refreshAccessToken.tsx:
const nextConfig = { images: { domains: ["localhost"], }, };
- getArticles.tsx:负责获取数据库中保存的所有文章的函数:
export { default } from "next-auth/middleware"; export const config = { matcher: ["/", "/newArticle/", "/article/", "/article/:path*"], };
- postArticle.tsx:负责将文章数据提交到我们的服务器的函数。
.env.local NEXTAUTH_SECRET = SubsTituaPorToken
- editArticle.tsx:负责修改数据库中特定文章的函数。
import NextAuth from "next-auth/next"; import type { AuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import { authenticate } from "@/services/authService"; import refreshAccessToken from "@/services/refreshAccessToken"; export const authOptions: AuthOptions = { providers: [ CredentialsProvider({ name: "credentials", credentials: { email: { name: "email", label: "email", type: "email", placeholder: "Email", }, password: { name: "password", label: "password", type: "password", placeholder: "Password", }, }, async authorize(credentials, req) { if (typeof credentials !== "undefined") { const res = await authenticate({ email: credentials.email, password: credentials.password, }); if (typeof res !== "undefined") { return { ...res }; } else { return null; } } else { return null; } }, }), ], session: { strategy: "jwt" }, secret: process.env.NEXTAUTH_SECRET, callbacks: { async jwt({ token, user, account }: any) { if (user && account) { return { token: user?.token, accessTokenExpires: Date.now() + parseInt(user?.expiresIn, 10), refreshToken: user?.tokenRefresh, }; } if (Date.now() <ol> <li> deleteArticle.tsx:负责从数据库中删除特定文章的函数。 </li> </ol> <pre class="brush:php;toolbar:false">'use client'; import React from 'react'; import { SessionProvider } from "next-auth/react"; export default function Provider({ children, session }: { children: React.ReactNode, session: any }): React.ReactNode { return ( <sessionprovider session="{session}"> {children} </sessionprovider> ) };
成分
接下来,让我们编写整个应用程序中使用的每个组件。
组件/Navbar.tsx
一个带有两个导航链接的简单组件。
/*global.css*/ .container { max-width: 1100px; width: 100%; margin: 0px auto; } .image-container { position: relative; width: 100%; height: 5em; padding-top: 56.25%; /* Aspect ratio 16:9 (dividindo a altura pela largura) */ } .image-container img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; } @keyframes spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .loading-spinner { width: 50px; height: 50px; border: 10px solid #f3f3f3; border-top: 10px solid #293d71; border-radius: 50%; animation: spinner 1.5s linear infinite; }
组件/Loading.tsx
一个简单的加载组件,在等待 API 调用完成时使用。
import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import Provider from "@/context/auth-provider"; import { getServerSession } from "next-auth"; import { authOptions } from "./api/auth/[...nextauth]/route"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Markdown Text Editor", description: "Created by ", }; export default async function RootLayout({ children, }: { children: React.ReactNode; }) { const session = await getServerSession(authOptions); return ( <provider session="{session}"> {children} </provider> ); }
组件/分页.tsx
我们页面上使用的分页组件,在我们的私有路径中显示我们的所有文章。您可以在这里找到有关如何编写此组件的更详细的文章
npm i npm run start
组件/ArticleCard.tsx
用于显示书面文章的卡片组件。
该组件还包含一个链接,该链接将指向文章显示页面和编辑先前撰写的文章的页面。
npx create-next-app myblog
组件/ArticleList.tsx
负责进行 API 调用并显示响应的组件。
在这里,我们将通过我们编写的函数使用两个 API 调用:
- getArticles.ts - 返回将在组件中显示的所有文章。
- removeArticle - 从我们的列表和服务器中删除特定的文章。
我们将使用之前编写的 Pagination.tsx 组件来跨页面分割文章数量。
npm i markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
页数
接下来,我们将按各自的路线划分浏览每个页面。
公共页面
登录
这是我们应用程序的主页。这是一个简单的页面,您可以根据需要对其进行修改。在这个页面中,我们将使用next-auth导航库提供的登录功能。
在文件 src/app/pages/public/login/page.tsx 中。
remark remark-gfm remark-react
文章页
为了创建文章阅读页面,我们将开发一个动态页面。
您访问过的每个博客平台可能都有一个用于阅读文章的专用页面,可通过 URL 访问。其原因是动态页面路由。幸运的是,Next.js 通过其新的 AppRouter 方法使这一切变得简单,使我们的生活变得更加简单。
首先:我们需要通过添加 [id] 文件夹在结构中创建路由。这将产生以下结构:pages/(public)/articles/[id]/pages.tsx.
- id 对应于我们导航路线的 slug。
- params 是通过包含导航 slug 的应用程序树传递的属性。
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
第二:使用MarkdownIt库,使页面能够显示Markdown格式的文本。
npm i react-icons @types/react-icons
最后,
页面准备好后,例如通过在浏览器中访问 localhost:3000/articles/1,您将能够使用提供的 ID 查看文章。
在我们的例子中,当单击其中一个 ArticleCards.tsx 组件时,ID 将通过导航传递,该组件将呈现在我们的私有路由的主页上。
src- |- app/ | |-(pages)/ | | |- (private)/ | | | |- (home) | | | |- editArticle/[id] | | | | | | | |- newArticle | | | - (public)/ | | | - article/[id] | | | - login | | | api/ | |- auth/[...nextAuth]/route.ts | |- global.css | |- layout.tsx | | - components/ | - context/ | - interfaces/ | - lib/ | - services/ middleware.ts
私人页面
这是我们的私人页面,只有用户在我们的应用程序中通过身份验证后才能访问。
家
在我们的app/pages/文件夹中,当在()内声明一个文件时,就意味着该路由对应于/。
在我们的例子中,(Home) 文件夹指的是我们私人路线的主页。这是用户在系统中进行身份验证后看到的第一个页面。此页面将显示我们数据库中的文章列表。
数据将由我们的 ArticlesList.tsx 组件处理。如果您还没有编写此代码,请参阅组件部分。
在应用程序/(页面)/(私人)/(主页)/page.tsx中。
npm i npm run start
新文章
这是我们应用程序中最重要的页面之一,因为它允许我们注册我们的文章。
此页面将使用户能够:
- 以 Markdown 格式写一篇文章。
- 为文章分配图像。
- 在将 Markdown 文本提交到服务器之前预览它。
页面使用了多个钩子:
- useCallback - 用于记忆函数。
- useState - 允许您向我们的组件添加状态变量。
- useSession - 让我们检查用户是否经过身份验证并获取身份验证令牌。
为此,我们将使用两个组件:
- TextEditor.tsx:我们之前编写的文本编辑器。
- Preview.tsx:用于显示 Markdown 格式文件的组件。
在构建此页面时,我们将使用我们的 API:
- POST:使用我们的函数 postArticle,我们将把文章发送到服务器。
我们还将使用 next-auth 库提供的 useSession 钩子来获取用户的身份验证令牌,该令牌将用于在服务器上注册文章。
这将涉及三个不同的 API 调用。
在 app/pages/(private)/newArticle/page.tsx.
“使用客户端”; 从“react”导入 React, { ChangeEvent, useCallback, useState }; 从“next-auth/react”导入{useSession}; 从“下一步/导航”导入{重定向}; 从“@/services/postArticle”导入 postArtical; 从“react-icons/ai”导入{AiOutlineFolderOpen}; 从“react-icons/ri”导入 { RiImageEditLine }; 从“下一个/图像”导入图像; 从“@/components/textEditor”导入文本编辑器; 从“@/components/PreviewText”导入预览; 从“react-icons/ai”导入{AiOutlineSend}; 从“react-icons/bs”导入{BsBodyText}; 导出默认函数 NewArticle(params:any) { const { 数据:会话 }:任何 = useSession({ 要求:真实, onUnauthenticated(){ 重定向(“/登录”); }, }); const [imageUrl, setImageUrl] = useState<object>({}); const [previewImage, setPreviewImage] = useState<string>(""); const [previewText, setPreviewText] = useState<boolean>(false); const [标题,setTitle] = useState<string>(""); const [doc, setDoc] = useState<string>("# Escreva o seu texto... n"); const handleDocChange = useCallback((newDoc: any) => { setDoc(newDoc); }, []); if (!session?.user) 返回 null; const handleArticleSubmit = async (e:any) =>; { e.preventDefault(); const token: string = session.user.token; 尝试 { const res = 等待 postArtical({ id: session.user.userId.toString(), 令牌:令牌, 图片网址: 图片网址, 标题:“标题” 文档: 文档, }); console.log('re--->', res); 重定向('/成功'); } 捕获(错误){ console.error('提交文章时出错:', error); // 如果需要,处理错误 抛出错误; } }; const handleImageChange = (e: React.ChangeEvent<htmlinputelement>) =>; { if (e.target.files && e.target.files.length > 0) { const 文件 = e.target.files[0]; const url = URL.createObjectURL(文件); 设置预览图像(网址); setImageUrl(文件); } }; const handleTextPreview = (e: 任意) => { e.preventDefault(); setPreviewText(!previewText); }; 返回 ( <section classname="w-full h-full min-h-screenrelative py-8"> {预览文本&&( <div classname="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100 rounded-xl w-full max-w-[33em] z-30">; ; setPreviewText(!previewText)} >> </div>; )} <form classname="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-slate-50 drop-shadow-xl flex flex-col 间隙-2“> {“”} <div className=" flex justify- between items-center> <bsbodytext></bsbodytext>> 预览 按钮>{" "} 恩维亚尔·特克斯托 <aioutlinesend classname="w-5 h-5 group-hover:text-red-500"></aioutlinesend> 按钮> ; <div classname="header-wrapper flex flex-col gap-2"> <div classname="image-box"> {previewImage.length === 0 && ( <div classname="select-image"> <aioutlinefolderopen classname="w-7 h-7"></aioutlinefolderopen> 拖放图像 标签> <h4> 编辑文章 </h4> <p>与<em>新文章</em>(newArticle)类似的页面,但有一些差异。</p> <p>首先,我们定义一条动态路线,在其中接收 id 作为导航参数。这与文章阅读页面上所做的非常相似。 <br> app/(pages)/(private)/editArticle/[id]/page.tsx<br> </p> <pre class="brush:php;toolbar:false">“使用客户端”; 从“react”导入 React, { useState, useEffect, useCallback, useRef, ChangeEvent }; 从“next-auth/react”导入{useSession}; 从“下一步/导航”导入{重定向}; 从“下一个/图像”导入图像; 从“@/interfaces/article.interface”导入{IArticle}; 从“react-icons/ai”导入{AiOutlineEdit}; 从“react-icons/bs”导入{BsBodyText}; 从“react-icons/ai”导入{AiOutlineFolderOpen}; 从“react-icons/ri”导入 { RiImageEditLine }; 从“@/components/PreviewText”导入预览; 从“@/components/textEditor”导入文本编辑器; 从'@/components/Loading'导入加载; 从“@/services/editArticle”导入 editArtical; 导出默认函数 EditArticle({ params }: { params: any }) { const { 数据:会话 }:任何 = useSession({ 要求:真实, onUnauthenticated(){ 重定向(“/登录”); }, }); const id: 数字 = params.id; const [文章,setArticle] = useState<iarticle>(空); const [imageUrl, setImageUrl] = useState<object>({}); const [previewImage, setPreviewImage] = useState<string>(""); const [previewText, setPreviewText] = useState<boolean>(false) const [标题,setTitle] = useState<string>(""); const [doc, setDoc] = useState<string>(''); const handleDocChange = useCallback((newDoc: any) => { setDoc(newDoc); }, []); const inputRef= useRef<htmlinputelement>(null); const fetchArticle = async (id: number) =>; { 尝试 { 常量响应 = 等待获取( `http://localhost:8080/articles/getById/${id}`, ); const jsonData = 等待响应.json(); setArticle(jsonData); } 捕获(错误){ console.log("出了点问题:", err); } }; useEffect(() => { if (文章 !== null || 文章 !== 未定义) { 获取文章(id); } }, [ID]); useEffect(()=>{ if(文章!= null && 文章.内容){ setDoc(文章.内容) } if(文章!=null && 文章.image){ setPreviewImage(`http://localhost:8080/` 文章.image) } },[文章]) const handleArticleSubmit = async (e:any) =>; { e.preventDefault(); const token: string = session.user.token; 尝试{ const res = 等待 editArtical({ 身份证号: 身份证号, 令牌:令牌, 图片网址:图片网址, 标题: 标题, 文档: 文档, }); console.log('re--->',res) 返回资源; } 捕获(错误){ console.log(“错误:”,错误) } }; const handleImageClick = ()=>{ console.log('hiii') if(inputRef.current){ inputRef.current.click(); } }const handleImageChange = (e: React.ChangeEvent<htmlinputelement>) =>; { if (e.target.files && e.target.files.length > 0) { const 文件 = e.target.files[0]; const url = URL.createObjectURL(文件); 设置预览图像(网址); setImageUrl(文件); } }; const handleTextPreview = (e: 任意) => { e.preventDefault(); setPreviewText(!previewText); console.log('预览版你好!') }; if(!article) return > if(文章?.内容) 返回 ( <section classname="w-full h-full min-h-screenrelative py-8"> {预览文本&&( <div classname="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100 rounded-xl w-full max-w-[33em] z-30">; ; setPreviewText(!previewText)} >> </div> )} <div classname="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-white drop- Shadow-md flex flex-col 间隙-2"> <form classname="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-slate-50 drop-shadow-md flex flex-col 间隙-2 "> {“”} <div classname="flex justify- Between items-center">; <bsbodytext></bsbodytext>> 预览 按钮>{" "} 编辑阿蒂戈 <aioutlineedit classname="w-5 h-5 group-hover:text-red-500"></aioutlineedit>> 按钮> </div>; <div classname="header-wrapper flex flex-col gap-2">; <div classname="image-box">; {previewImage.length === 0 && ( <div classname="select-image">; <aioutlinefolderopen classname="w-7 h-7"></aioutlinefolderopen>> 拖放图像 标签> <h2> 结论 </h2> <p>首先,我要感谢您花时间阅读本教程,并且我还要祝贺您完成它。我希望它对您有帮助,并且分步说明很容易遵循。</p> <p>其次,我想强调一下关于我们刚刚构建的内容的几点。这是博客系统的基础,还有很多东西需要添加,比如显示所有文章的公共页面、用户注册页面,甚至是自定义的 404 错误页面。如果在教程期间您对这些页面感到好奇并错过了它们,请知道这是故意的。本教程为您提供了足够的经验来自行创建这些新页面、添加许多其他页面以及实现新功能。</p> <p>非常感谢,下次再见。哦/</p> </div> </div> </div> </form> </div></section></htmlinputelement></htmlinputelement></string></string></boolean></string></object></iarticle>
以上是使用 Next.js 构建动态博客仪表板的详细内容。更多信息请关注PHP中文网其他相关文章!

JavaScript字符串替换方法详解及常见问题解答 本文将探讨两种在JavaScript中替换字符串字符的方法:在JavaScript代码内部替换和在网页HTML内部替换。 在JavaScript代码内部替换字符串 最直接的方法是使用replace()方法: str = str.replace("find","replace"); 该方法仅替换第一个匹配项。要替换所有匹配项,需使用正则表达式并添加全局标志g: str = str.replace(/fi

本教程向您展示了如何将自定义的Google搜索API集成到您的博客或网站中,提供了比标准WordPress主题搜索功能更精致的搜索体验。 令人惊讶的是简单!您将能够将搜索限制为Y

因此,在这里,您准备好了解所有称为Ajax的东西。但是,到底是什么? AJAX一词是指用于创建动态,交互式Web内容的一系列宽松的技术。 Ajax一词,最初由Jesse J创造

本文系列在2017年中期进行了最新信息和新示例。 在此JSON示例中,我们将研究如何使用JSON格式将简单值存储在文件中。 使用键值对符号,我们可以存储任何类型的

利用轻松的网页布局:8个基本插件 jQuery大大简化了网页布局。 本文重点介绍了简化该过程的八个功能强大的JQuery插件,对于手动网站创建特别有用

核心要点 JavaScript 中的 this 通常指代“拥有”该方法的对象,但具体取决于函数的调用方式。 没有当前对象时,this 指代全局对象。在 Web 浏览器中,它由 window 表示。 调用函数时,this 保持全局对象;但调用对象构造函数或其任何方法时,this 指代对象的实例。 可以使用 call()、apply() 和 bind() 等方法更改 this 的上下文。这些方法使用给定的 this 值和参数调用函数。 JavaScript 是一门优秀的编程语言。几年前,这句话可

jQuery是一个很棒的JavaScript框架。但是,与任何图书馆一样,有时有必要在引擎盖下发现发生了什么。也许是因为您正在追踪一个错误,或者只是对jQuery如何实现特定UI感到好奇

该帖子编写了有用的作弊表,参考指南,快速食谱以及用于Android,BlackBerry和iPhone应用程序开发的代码片段。 没有开发人员应该没有他们! 触摸手势参考指南(PDF) Desig的宝贵资源


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

mPDF
mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),

安全考试浏览器
Safe Exam Browser是一个安全的浏览器环境,用于安全地进行在线考试。该软件将任何计算机变成一个安全的工作站。它控制对任何实用工具的访问,并防止学生使用未经授权的资源。

MinGW - 适用于 Windows 的极简 GNU
这个项目正在迁移到osdn.net/projects/mingw的过程中,你可以继续在那里关注我们。MinGW:GNU编译器集合(GCC)的本地Windows移植版本,可自由分发的导入库和用于构建本地Windows应用程序的头文件;包括对MSVC运行时的扩展,以支持C99功能。MinGW的所有软件都可以在64位Windows平台上运行。

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3 Linux新版
SublimeText3 Linux最新版