ns-framework
v1.0.8
Published
A lightweight Vue-like micro framework for Chrome extensions and browser apps
Maintainers
Readme
NS Framework
Language: English | Tiếng Việt | 日本語 | 中文
NS Framework 是一个专为 Chrome 扩展打造的超轻量迷你框架,语法接近 Vue,但完全不需要构建工具。
主要特性
- ⚡ 无外部依赖
- 🔄 基于 Proxy 的实时绑定
- 🛡️ 兼容 Manifest V3 / CSP
- 🧩 通过 ES Module 支持组件、props、插槽(默认 + 具名)
- 🔒 每个组件独立的作用域隔离
- 🧮 computed 计算属性(缓存、自动失效、支持 setter)
- 👀 watch、接近 Vue 的生命周期钩子
- 🧠 大量接近 Vue 语法的指令
安装
npm
npm i ns-framework原生 JS
将 lib/ns.js 复制到扩展的 lib 目录,然后在 HTML 中加载:
<script src="./lib/ns.js"></script>基本用法
<div id="app"></div>
<script src="./lib/ns.js"></script>
<script src="./popup.js"></script>// popup.js
new NS({
el: "#app",
data: {
title: "Hello NS",
count: 0,
isVisible: true
},
methods: {
increment() {
this.count += 1;
}
},
mounted() {
console.log("Ready");
},
template: `
<div>
<h1 ns-text="title"></h1>
<p>Count: <span ns-text="count"></span></p>
<button ns-click="increment">Increment</button>
<div ns-show="isVisible">Visible block</div>
</div>
`
});支持的指令
Text / HTML
<span ns-text="name"></span>
<p ns-html="message"></p>指令中的表达式只是一条(可嵌套的)属性路径,而不是任意 JS 表达式:
<span ns-text="user.profile.name"></span>
<div ns-if="user?.profile?.name"></div> <!-- 支持可选链 "?.",效果等同于 "." -->ns-text / ns-if / ns-show 等不能在模板里直接运行三元表达式或比较运算,比如 a ? b : c、count > 5 - 请用 computed 预先算好结果,再绑定到对应的属性上。
Props 与组件
⚠️ 重要提示:HTML 解析时会将属性名统一转为小写(不仅是
.html文件,.js文件里通过innerHTML赋值的template字符串同样受影响)。在props中以驼峰命名(camelCase)声明的多单词 prop,在模板中用:绑定时必须写成 kebab-case(短横线分隔),否则属性名会丢失大小写信息,无法与 prop 匹配上。
<ns-header title="WebBlock | MLight"></ns-header>
<ns-footer :app-config-info="appConfigInfo"></ns-footer>// footer.js
export default {
props: {
title: {
type: String, // "type" 仅作文档说明用,不会被校验或做类型转换
default: "MLight" // 父组件未传值时会使用 "default"
},
appConfigInfo: {
type: Object
}
},
template: `
<div>
<h1 ns-text="title"></h1>
</div>
`
};props 也可以声明为一个名字数组(无需 default/type):props: ["title", "appConfigInfo"]。
对象/数组类型的 prop 与父组件共享同一引用:如果你直接修改(mutate)对象内部的字段(this.appConfigInfo.author.name = "x"),改动会自动传播并在子组件中正确重新渲染(包括依赖该字段的 computed)。但如果父组件整体重新赋值这个 prop(用一个全新对象赋值 this.appConfigInfo = {...},或修改 string/number/boolean 类型的 prop),子组件不会自动重新同步 - props 目前只在挂载时读取一次。
Computed
computed: {
// 简单 getter 形式 - 有缓存,依赖项变化时自动重新计算
// (包括嵌套路径,以及从父组件按引用共享的 prop)
isTextEmpty() {
return !this.input.text.value;
},
// 带 setter 的形式 - 给 this.fullName 赋值会调用 set()
fullName: {
get() {
return `${this.first} ${this.last}`;
},
set(value) {
const [first, last] = value.split(" ");
this.first = first;
this.last = last;
}
}
}<div ns-show="isTextEmpty">为空</div>
<span ns-text="fullName"></span>Watch
watch: {
// 当 "count" 属性变化时调用
count(newValue, oldValue) {
console.log(newValue, oldValue);
},
// "*" 会捕获所有属性变化(顶层与嵌套均可)
"*": (newValue, oldValue) => {
console.log("changed:", newValue, oldValue);
}
}插槽(默认 + 具名)
// 子组件
template: `
<div class="input-group">
<slot name="before"></slot>
<slot></slot>
<slot name="after"></slot>
</div>
`<!-- 父组件 -->
<my-input>
<i slot="before" class="fa fa-search"></i>
<span>默认内容</span>
<button slot="after">Clear</button>
</my-input>如果希望插槽出口是普通元素而不是 <slot> 标签,可以用 ns-slot="before" 代替 <slot name="before">。
组件的 emit 与自定义事件
// 子组件
methods: {
save() {
this.$emit("saved", { ok: true });
}
}<!-- 父组件:用 @event="handler" 绑定(推荐) -->
<child-component @saved="onSaved"></child-component>// 父组件
methods: {
onSaved(payload) {
console.log(payload);
}
}如果不绑定 @event,父组件上与事件同名的方法(saved())仍会自动被调用(向后兼容)- 但建议使用 @event="handler" 来指定一个不同的方法名,避免多个实例 emit 同一个事件名时互相冲突。
组件上的 v-model(双向绑定)
<child-component v-model="value"></child-component>子组件通过 modelValue 这个 prop 接收值,并通过 this.$emit("update:modelValue", newValue) 把变化传回父组件。
双向绑定
<input ns-model="name" />
<textarea ns-model="description"></textarea>Model 修饰符
<input ns-model="name.trim" />
<input ns-model="age.number" />
<input ns-model="search.lazy" />
<input ns-model="query.debounce300" />
<input ns-model="title.capitalize" />
<input ns-model="email.lowercase" />支持的元素:
- checkbox
- radio
- select
- select multiple
事件
<button ns-click="save"></button>
<button ns-click="save.prevent.stop"></button>
<button @click="save"></button>
<div ns-on="input:updateValue"></div>
<div @input="updateValue"></div>条件渲染
<div ns-if="isLoggedIn">已登录</div>
<div ns-else-if="isLoading">加载中...</div>
<div ns-else>未登录</div>列表渲染
同时支持数组和普通对象(item/key),带不带 Vue 风格的括号都可以:
<ul>
<li ns-for="item in items" ns-text="item.name"></li>
<li ns-for="(item, index) in items" ns-text="item.name"></li>
<li ns-for="(value, key) in someObject" ns-text="key"></li>
</ul>Class / Style / 属性绑定
<div ns-class="className"></div>
<div ns-class="{ active: isActive, disabled: isDisabled }"></div>
<div ns-style="styleObject"></div>
<a ns-bind="href:linkUrl"></a>
<a :href="linkUrl"></a>配置项
| 选项 | 说明 |
| --- | --- |
| el | 选择器或 DOM 元素 |
| data | 对象或函数(组件推荐使用函数,避免多个实例共享状态) |
| template | HTML 字符串,会覆盖 el 的内容 |
| methods | 包含方法的对象,方法内的 this 指向响应式数据 |
| computed | getter(或 {get, set})组成的对象,带缓存并自动失效 |
| watch | 监听属性变化的对象(按 key 或 "*") |
| components | 子组件注册表({ "tag-name": ComponentConfig }) |
| props | 组件的 prop 声明(名字数组,或带 type/default 的对象) |
| stateChanged | (prop, value) => {},每当某个属性的值发生变化时调用 |
| beforeCreate / created | 响应式数据初始化前/后触发的钩子 |
| beforeMount / mounted | 首次渲染并挂载 DOM 前/后触发的钩子 |
| beforeUpdate / updated | 每次因状态变化而重新渲染前/后触发的钩子 |
生命周期顺序
beforeCreate → (创建响应式 Proxy) → created → (绑定 methods)
→ (加载模板、渲染插槽、挂载子组件、首次渲染、绑定事件)
→ beforeMount → mounted每次属性变化时:beforeUpdate → (整个组件子树重新渲染) → updated → stateChanged。
组件示例
// Header.js
export default {
template: `
<header>
<h1 ns-text="title"></h1>
</header>
`,
data() {
return {
title: "Header Component"
};
}
};import Header from "./Header.js";
new NS({
el: "#app",
components: {
"app-header": Header
}
});<div id="app">
<app-header></app-header>
</div>
<script type="module" src="./main.js"></script>说明
- 使用 ES Module 时,请通过本地服务器加载,或以已解压的扩展方式加载。
- 在 Chrome 扩展中,逻辑代码应放在独立的 JS 文件中,而不是内联脚本。
- 自定义元素(
<my-component>)在 HTML 中不能用<my-component />自闭合(HTML 不会尊重普通元素上的自闭合写法)- 请始终显式写成<my-component></my-component>。 ns-for上的:key只是一个普通的 HTML 属性,框架目前并未用它来做 DOM 差异对比/复用 - 每次渲染时整个列表都会从头重建。
