工作中遇到个特殊的需求,项目要求改造一些老框架平台的样式。有多个平台要改,色彩主题是通用的,但是不同平台之间有一些小的差别。因为代码比较老,很多地方都是没办法用react、vue封装好的组件的,而且要求只改样式,不能动其他地方。
我们把样式写在我们的 monorepo 组件库里面,其他地方直接引用就行了
但是遇到一个问题,我们是用 less 写的,本地开发调试的时候预览比较麻烦,要反复跑编译的命令,所以就想整一个本地脚手架服务,自动监听 less 文件变更,然后输出单独的 css 链接地址,引用这个本地地址就行,页面只要刷新一下就能加载最新的样式
实现方法有很多,之前用 vite 写插件的时候觉得挺方便的,第一时间就先用 vite 写个本地插件,很简单的解决了这个问题
vite 插件实现
我们仓库里面 vite 跑的是 react 的模板,其他地方就不讲了,直接创建一个 plugins 文件夹,创建一个 online-less-server.ts 文件
这是我为了演示本地快速跑的 vite 脚手架,其他文件都是 vite 自带的引导创建的
第一个就是需要提供本地 less 编译服务的源文件,我们需要把 less 文件夹下面的所有 less 文件编译成 css,并且能直接通过一个本地的 URL 就能够访问到
如果你是第一次创建这个 react 模板,那还没办法直接用 less 文件,安装一下 less 就行,不需要做其他处理,vite 都处理好了
npm i less
不过,因为创建的插件是 ts 格式的,需要标记一下目录,让 ts 将对应文件转译成 js,打开 tsconfig.node.json
javascript
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts", "plugins"]
}
这里只改了 include,增加了 plugins 这个目录
接下来就是插件代码了,我们打开 online-less-server.ts,根据约定,文件需要默认导出一个函数
javascript
export default function lessPlugin() {
return {
name: 'online-less-server',
}
}
我们需要知道的是,如果我们的页面代码里面用到了 less 类型的文件,在已经安装了 less 的情况下,vite 是已经将对应的文件处理好了的,不需要我们再去处理。所以这里我的想法就是拿到这个已经处理好的文件
怎么拿呢
我是通过 transform 这个钩子拿的
javascript
let lessCodeMap = {}
export default function lessPlugin() {
return {
name: 'online-less-server',
// 在其他钩子中使用存储的配置
transform(code, id) {
if (/\.less\??[^.]*$/.test(id)) {
// 使用 lessCodeMap 存储 code
}
},
}
}
transform 函数会传两个参数进来,第一个是编译好的代码,第二个是资源的源路径
这里用了一个正则去匹配以 less 结尾的文件。当然,如果我们仅针对特定目录下的 less 文件,就需要改造一下正则
javascript
let lessCodeMap = {}
export default function lessPlugin() {
return {
name: 'online-less-server',
// 在其他钩子中使用存储的配置
transform(code, id) {
let n
if (n = /\/less\/[A-Za-z\-]*\.less\??[^.]*$/.exec(id)) {
lessCodeMap[n[0].split('/')[2]] = code;
}
},
}
}
这里仅供参考,大家根据自己的文件结构来
这样我们就有了一个存储编译后 css 代码的对象,它的结构是这样的
javascript
{
"文件名": "代码"
···
}
接下来是怎么使用这个对象
我们需要另外一个钩子,能够帮我们拦截指定路径的请求,我们拦截后返回对应的代码内容
这个钩子是 configureServer
javascript
let lessCodeMap = {}
export default function lessPlugin() {
return {
name: 'online-less-server',
configureServer(server) {
server.middlewares.use((req, res, next) => {
const pathList = req.url.split('/')
if (pathList[1] === 'online-less-server' && lessCodeMap[pathList[2]]) {
res.end(lessCodeMap[pathList[2]])
return
}
next()
})
},
// 在其他钩子中使用存储的配置
transform(code, id) {
let n
if (n = /\/less\/[A-Za-z\-]*\.less\??[^.]*$/.exec(id)) {
lessCodeMap[n[0].split('/')[2]] = code;
}
},
}
}
这里我们拦截了路径 /online-less-server 的请求,res.end 返回对应的代码内容,路径匹配不上就调用 next,让 vite 走默认处理
这样这个插件就写完了,代码很少,非常简单
我们在 vite.config.ts 中使用我们的插件
javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import lessPlugin from './plugins/online-less-server'
export default defineConfig({
plugins: [react(), lessPlugin()],
})
还没结束,刚才也提到了,我们是用了 vite 已经帮我们处理好的 less,也就是这些 less 文件必须明确在代码中用到或者说引入了,vite 才会帮我们处理。我们需要在一个文件中引入所有需要使用的 less
如果你和我一样把 less 文件夹放在最外层的话,直接引入 less 会引起 ts 报错,如果报错了需要改一下 tsconfig.json
javascript
"include": ["src", "less"]
其他地方不改,只改 include
然后我们就可以测试我们的本地 less 自动编译服务了,随便写几行 less
css
@default-color: aqua;
.root {
font-size: 30px;
color: @default-color;
.content {
background-color: white;
color: black;
}
}
直接用 node 实现
再讲一下不用 vite,直接用 node 和各种包本地启一个简单的 less 服务。优点是足够简单,但是缺点也是太简单了容易出问题,很多地方的细节需要自己去完善。
我们新建一个文件夹,然后在这个新文件夹下做一下 npm 的初始化
npm init
直接一直回车就行
首先我们创建一个 app.js,本地启动一个服务需要装几个包
npm i connect http
然后是启动服务的代码
css
const connect = require('connect')
const http = require('http')
const app = connect()
app.use(function (req, res) {
res.end('Hello from Connect!\n')
})
http.createServer(app).listen(3000)
为了方便,我们在 package.json 增加一段脚本
typescript
"scripts": {
"dev": "node app.js"
},
写完后我们直接在控制台 yarn dev / npm run dev 就行
这样子服务就算启起来了
接下来处理 less 代码,和上面一样,我们设置特定的路径 /online-less-server ,只在请求该路径下的 less 资源的时候我们在做处理,或者按自己的想法来
然后处理 less,需要安装额外的依赖
npm i less
typescript
const fs = require('fs')
try {
fs.readFile(lessInput, async (err, data) => {
if (!err) {
const text = data.toString()
const lessOutPut = await less.render(text, { filename: lessInput })
res.end(lessOutPut.css)
} else throw err
})
} catch {
res.end('Error')
}
这部分可以就是 less 函数的用法了,可以参考 less 的官网
lessOutPut.css 就是最终转换好的 css ,res.end 将内容返回给客户端,整理一下,完整代码如下
typescript
const connect = require('connect')
const http = require('http')
const fs = require('fs')
const path = require('path')
const less = require('less')
const app = connect()
const lessPath = './less' // less 的文件夹(自定义)
app.use(function (req, res) {
const pathList = req.url.split('/')
if (pathList[1] === 'online-less-server') {
try {
const target = path.join(lessPath, pathList[2])
fs.readFile(target, async (err, data) => {
if (!err) {
const text = data.toString()
const lessOutPut = await less.render(text, { filename: target })
res.end(lessOutPut.css)
} else throw err
})
} catch {
res.end('Error')
}
return;
}
res.end('Hello from Connect!\n')
})
http.createServer(app).listen(3000)
接下来写两个 less 文件,需要新建一个 less 文件夹
css
// global.less
@default-font-colot: black;
@default-btn-colot: rgb(102, 156, 255);
// a.less
@import './global.less';
.root {
display: block;
font-size: 20px;
.content {
background-color: white;
color: @default-font-colot;
font-weight: bold;
}
.btn {
background-color: @default-btn-colot;
&:hover {
color: white;
}
}
}
执行 yarn dev / npm run dev,把服务跑起来,输入指定的地址
查看结果
可以看到,less 会自动帮我们处理引入的文件(global.less),然后输出